From c779dde8f2dda1684e90178ad1986da00e4349c4 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 8 Mar 2023 14:11:54 +0100 Subject: [PATCH 01/20] Unregister service worker --- src/index.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.tsx b/src/index.tsx index 840c46c..6733448 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,9 @@ main(); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA +serviceWorker.unregister(); + +/* serviceWorker.register( { onSuccess: () => { @@ -49,3 +52,4 @@ serviceWorker.register( }, } ); +*/ From 154927404f361d91e154f933f7eb4f55a2aeda59 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 8 Mar 2023 14:12:16 +0100 Subject: [PATCH 02/20] Do not use "base" in index.html --- public/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/index.html b/public/index.html index 75c34b2..d9dbd46 100644 --- a/public/index.html +++ b/public/index.html @@ -12,7 +12,7 @@ --> Cate App - + From 2a44b324063ef6ac31f3a5f75e2cc5718f3367e5 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sat, 18 Mar 2023 14:28:35 +0100 Subject: [PATCH 03/20] no longer using login, hub, and routes --- README.md | 3 +- package.json | 4 - src/config.ts | 82 -------- src/index.tsx | 34 --- src/renderer/actions.ts | 168 --------------- src/renderer/containers/AppBar.tsx | 88 +------- src/renderer/containers/AppMainPage.tsx | 4 +- src/renderer/containers/AppMainPageForHub.tsx | 49 ----- src/renderer/containers/AppMainPageForSA.tsx | 55 ----- src/renderer/containers/AppModePage.tsx | 194 ------------------ src/renderer/containers/AppRouter.tsx | 46 ----- src/renderer/containers/PreferencesDialog.tsx | 2 +- src/renderer/containers/UserMenu.tsx | 61 ------ src/renderer/containers/VersionTags.tsx | 2 +- src/renderer/containers/WebAPIStatusBox.tsx | 111 +--------- src/renderer/initial-state.ts | 5 - src/renderer/main.tsx | 69 +------ src/renderer/reducers.ts | 16 -- src/renderer/state.ts | 7 - .../webapi/apis/ServiceProvisionAPI.ts | 123 ----------- src/serviceWorker.ts | 166 --------------- src/version.ts | 5 + yarn.lock | 143 +------------ 23 files changed, 28 insertions(+), 1409 deletions(-) delete mode 100644 src/config.ts delete mode 100644 src/renderer/containers/AppMainPageForHub.tsx delete mode 100644 src/renderer/containers/AppMainPageForSA.tsx delete mode 100644 src/renderer/containers/AppModePage.tsx delete mode 100644 src/renderer/containers/AppRouter.tsx delete mode 100644 src/renderer/containers/UserMenu.tsx delete mode 100644 src/renderer/webapi/apis/ServiceProvisionAPI.ts delete mode 100644 src/serviceWorker.ts create mode 100644 src/version.ts diff --git a/README.md b/README.md index 45398c5..e22098d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ To prepare a Cate App release, follow these steps: - [ ] `package.json`: Make sure `version` field is higher than the last release and compatible with [SemVer](https://semver.org/). -- [ ] `src/serviceWorker.ts`: Make sure `CATE_PWA_VERSION` constant is the same version. -- [ ] `src/config.ts`: Make sure `CATE_APP_VERSION` constant is the same version. +- [ ] `src/version.ts`: Make sure `CATE_APP_VERSION` constant is the same version. - [ ] `appveyor.yml`: Make sure `version` label is the same version appended by `-{build}`. - [ ] `CHANGES.md`: Make sure latest changes are up-to-date and refer to the same version. diff --git a/package.json b/package.json index 111c7f0..574b31e 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,6 @@ "@blueprintjs/select": "^3.13.6", "@blueprintjs/table": "^3.8.12", "@datapunt/matomo-tracker-react": "^0.3.1", - "@react-keycloak/web": "^1.0.6", "@types/js-cookie": "^2.2.6", "cesium": "1.71.0", "classnames": "^2.2.6", @@ -19,7 +18,6 @@ "deep-equal": "^1.0.1", "deep-object-diff": "^1.1.0", "js-cookie": "^2.2.1", - "keycloak-js": "^8.0.2", "normalize.css": "^8.0.1", "react": "^16.13.1", "react-ace": "^5.9.0", @@ -29,7 +27,6 @@ "react-dropzone": "^11.0.2", "react-markdown": "^6.0.2", "react-redux": "^5.1.2", - "react-router-dom": "^5.2.0", "react-transition-group": "^1.2.1", "redux": "^3.7.2", "redux-logger": "^3.0.6", @@ -61,7 +58,6 @@ "@types/react-dom": "^16.9.0", "@types/react-dropzone": "^5.1.0", "@types/react-redux": "^5.0.21", - "@types/react-router-dom": "^5.1.5", "@types/react-test-renderer": "^16.8.1", "@types/redux-logger": "^3.0.7", "@types/tether": "^1.4.4", diff --git a/src/config.ts b/src/config.ts deleted file mode 100644 index ea0a662..0000000 --- a/src/config.ts +++ /dev/null @@ -1,82 +0,0 @@ -// IMPORTANT: Any changes of CATE_APP_VERSION must be synchronized with -// 1. with the version field in "../package.json". -// 2. with CATE_PWA_VERSION in "./serviceWorker.ts" -// -export const CATE_APP_VERSION = "3.1.5-dev.0"; - -const DEFAULT_API_ENDPOINT_DEV = 'https://dev.catehub.brockmann-consult.de/api/v2/'; -const DEFAULT_API_ENDPOINT_PRODUCTION = 'https://catehub.climate.esa.int/api/v2'; -const DEFAULT_API_ENDPOINT_STAGE = 'https://stage.catehub.climate.esa.int/api/v2'; - -const MANAG_API_PATH = '/users/{username}/webapis'; -const CLOSE_API_PATH = '/users/{username}/webapis'; -const COUNT_API_PATH = '/webapis'; - -const DEFAULT_MAX_NUM_USERS = 50; - -const DEFAULT_AUTH_URL = "https://192-171-169-159.sslip.io/auth"; -const DEFAULT_AUTH_REALM = "cate"; -const DEFAULT_AUTH_CLIENT_ID = "cate-webui"; - - -export interface AuthConfig { - url: string; - realm: string; - clientId: string; -} - -export interface WebApiConfig { - endpointUrl: string; - managApiUrl: string; - closeApiUrl: string; - countApiUrl: string; - maxNumUsers: number; -} - -export interface Config { - version: string; - auth: AuthConfig; - webApi: WebApiConfig; -} - -const ENDPOINT_URL = getEndpointUrl(); - -export const CONFIG: Config = { - version: CATE_APP_VERSION, - auth: { - url: process.env.REACT_APP_KEYCLOAK_URL || DEFAULT_AUTH_URL, - realm: process.env.REACT_APP_KEYCLOAK_REALM || DEFAULT_AUTH_REALM, - clientId: process.env.REACT_APP_KEYCLOAK_CLIENT_ID || DEFAULT_AUTH_CLIENT_ID, - }, - webApi: { - endpointUrl: ENDPOINT_URL, - managApiUrl: ENDPOINT_URL + MANAG_API_PATH, - closeApiUrl: ENDPOINT_URL + CLOSE_API_PATH, - countApiUrl: ENDPOINT_URL + COUNT_API_PATH, - maxNumUsers: getMaxNumUsers(), - } -}; - -console.debug('CONFIG:', CONFIG); - - -function getEndpointUrl(): string { - let url = process.env.REACT_APP_CATEHUB_ENDPOINT; - if (!url) { - if (window.location.host.indexOf('stage') >= 0) { - url = DEFAULT_API_ENDPOINT_STAGE; - } else if (window.location.host.indexOf('dev') >= 0) { - url = DEFAULT_API_ENDPOINT_DEV; - } else { - url = DEFAULT_API_ENDPOINT_PRODUCTION; - } - } - return url.endsWith('/') ? url.substr(0, url.length - 1) : url; -} - -function getMaxNumUsers(): number { - return process.env.REACT_APP_MAX_NUM_USERS - ? parseInt(process.env.REACT_APP_MAX_NUM_USERS) - : DEFAULT_MAX_NUM_USERS; -} - diff --git a/src/index.tsx b/src/index.tsx index 6733448..25504a6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,4 @@ import { main } from './renderer/main'; -import { showToast } from './renderer/toast' -import * as serviceWorker from './serviceWorker'; import 'normalize.css/normalize.css'; import '@blueprintjs/core/lib/css/blueprint.css'; @@ -21,35 +19,3 @@ declare global { window.CESIUM_BASE_URL = './cesium'; main(); - -// If you want your app to work offline and load faster, you can change -// unregister() to register() below. Note this comes with some pitfalls. -// Learn more about service workers: https://bit.ly/CRA-PWA -serviceWorker.unregister(); - -/* -serviceWorker.register( - { - onSuccess: () => { - showToast({ - type: 'notification', - text: 'App is cached for offline use.' - }); - }, - onUpdate: () => { - showToast({ - type: 'notification', - text: 'New app version available. ' + - 'It will be used when all tabs for this page are closed.' - }); - }, - onError: (error: Error) => { - showToast({ - type: 'error', - text: 'Error during service worker registration: ' + - error.message - }); - }, - } -); -*/ diff --git a/src/renderer/actions.ts b/src/renderer/actions.ts index b96cfcc..ebf1544 100644 --- a/src/renderer/actions.ts +++ b/src/renderer/actions.ts @@ -2,8 +2,6 @@ import * as Cesium from 'cesium'; import copyToClipboard from 'copy-to-clipboard'; import * as d3 from 'd3-fetch'; import { DirectGeometryObject } from 'geojson'; -import { History } from 'history'; -import { KeycloakInstance, KeycloakProfile } from 'keycloak-js'; import * as redux from 'redux'; import * as assert from '../common/assert'; @@ -98,9 +96,7 @@ import { WebAPIClient } from './webapi'; import { ServiceInfoAPI } from './webapi/apis/ServiceInfoAPI'; -import { ServiceProvisionAPI, PodStatus } from './webapi/apis/ServiceProvisionAPI'; import { HttpError } from './webapi/HttpError'; -import { CONFIG } from '../config'; const electron = requireElectron(); @@ -153,135 +149,6 @@ export const INVOKE_CTX_OPERATION = 'INVOKE_CTX_OPERATION'; export const SET_USER_PROFILE = 'SET_USER_PROFILE'; -const SECOND = 1000; -const MINUTE = 60 * SECOND; - -export function launchWebAPIService(keycloak: KeycloakInstance<'native'>): ThunkAction { - return async (dispatch: Dispatch) => { - dispatch(setWebAPIStatus('login')); - - if (!keycloak.authenticated) { - await keycloak.login({ - redirectUri: window.location.origin + '/hub', - prompt: 'login', - maxAge: 86400, // seconds = 24h - }); - } - - const userProfile = await keycloak.loadUserProfile(); - dispatch(_setUserProfile(userProfile)); - - const token = keycloak!.token; - // console.log("Token: ", token); - - const serviceProvisionAPI = new ServiceProvisionAPI(); - - let serviceCount; - try { - serviceCount = await serviceProvisionAPI.getServiceCount(); - } catch (error) { - dispatch(setWebAPIStatus('error')); - handleFetchError(error, 'Failed fetching server status.'); - return; - } - if (serviceCount >= CONFIG.webApi.maxNumUsers) { - showToast({type: 'error', text: 'Too many concurrent users. Please try again later!'}); - dispatch(setWebAPIStatus('error')); - return; - } - - dispatch(setWebAPIStatus('launching')); - let serviceURL; - try { - serviceURL = await serviceProvisionAPI.startService(userProfile.username, token); - } catch (error) { - dispatch(setWebAPIStatus('error')); - handleFetchError(error, 'Launching of Cate service failed.'); - return; - } - - function isPodRunning(serviceStatus: PodStatus | null) { - return serviceStatus && serviceStatus.phase === 'Running'; - } - - const podStatus = await serviceProvisionAPI.getPodStatus(userProfile.username, token); - if (isPodRunning(podStatus)) { - dispatch(connectWebAPIService(serviceURL)); - } else { - const handleServiceError = (error: any) => { - handleFetchError(error, 'Launching of Cate service failed.'); - dispatch(setWebAPIStatus('error')); - }; - - const getPodStatus = async () => { - try { - return await serviceProvisionAPI.getPodStatus(userProfile.username, token); - } catch (error) { - return null; - } - }; - - invokeUntil(getPodStatus, - isPodRunning, - () => dispatch(connectWebAPIService(serviceURL)), - handleServiceError, - 2 * SECOND, - 5 * MINUTE); - } - } -} - -function _setUserProfile(userProfile: KeycloakProfile): Action { - return {type: SET_USER_PROFILE, payload: userProfile} -} - -export function logout(keycloak: KeycloakInstance<'native'>, history: History): ThunkAction { - return async (dispatch: Dispatch, getState: GetState) => { - const userProfile = getState().communication.userProfile; - const hubMode = Boolean(userProfile); - if (hubMode) { - try { - console.debug("Shutting down service..."); - dispatch(setWebAPIStatus('shuttingDown')); - const serviceProvisionAPI = new ServiceProvisionAPI(); - await serviceProvisionAPI.stopServiceInstance(userProfile.username); - } catch (error) { - // ok, we are closing down anyway - } - if (keycloak.authenticated) { - try { - dispatch(setWebAPIStatus('loggingOut')); - await keycloak.logout({redirectUri: window.location.origin}); - } catch (error) { - history.replace('/'); - } - } else { - history.replace('/'); - } - } else { - history.replace('/'); - } - } -} - -export function manageAccount(keycloak: KeycloakInstance<'native'>): ThunkAction { - return async (dispatch: Dispatch) => { - dispatch(savePreferences(() => { - const accountUrl = keycloak.createAccountUrl(); - const accountWindow = window.open(accountUrl, '_blank'); - if (accountWindow && typeof accountWindow.focus === 'function') { - accountWindow.focus(); - } - })); - } -} - -export function setWebAPIProvisionCateHub(keycloak: KeycloakInstance<'native'>): ThunkAction { - return (dispatch: Dispatch) => { - dispatch(launchWebAPIService(keycloak)); - }; -} - export function setWebAPIStatus(webAPIStatus: WebAPIStatus): Action { return {type: SET_WEBAPI_STATUS, payload: {webAPIStatus}}; } @@ -2340,10 +2207,6 @@ export const showPwaInstallPrompt = (): ThunkAction => (dispatch) => { }); } -export function updatePwaDisplayMode(pwaDisplayMode: string): Action { - return {type: UPDATE_PWA_DISPLAY_MODE, payload: pwaDisplayMode}; -} - ///////////////////////////////////////////////////////////////////////////////////// // EU GDPR actions @@ -2667,37 +2530,6 @@ export function handleFetchError(error: any, message: string) { showToast({type: 'error', text: message + suffix}); } -function invokeUntil(callback: () => Promise, - condition: (result: T) => boolean, - onSuccess: (result: T) => any, - onError: (error: any) => any, - interval: number, - timeout: number) { - let startTime = new Date().getTime(); - let func: () => void; - // Uncomment for debugging - // let attempt = 0; - let error: any = null; - const _func = async () => { - // attempt++; - let result; - try { - // console.log('attempt:', attempt); - result = await callback(); - } catch (e) { - error = e; - } - if (condition(result)) { - onSuccess(result); - } else if ((new Date().getTime() - startTime) > timeout) { - onError(error || new Error('Timeout')); - } else { - setTimeout(func, interval); - } - }; - func = _func; - setTimeout(_func, interval); -} let _handlersInstalled: boolean = false; diff --git a/src/renderer/containers/AppBar.tsx b/src/renderer/containers/AppBar.tsx index b4002c0..4f23746 100644 --- a/src/renderer/containers/AppBar.tsx +++ b/src/renderer/containers/AppBar.tsx @@ -1,20 +1,17 @@ -import { CSSProperties } from 'react'; import * as React from 'react'; import { connect, Dispatch } from 'react-redux'; -import { KeycloakProfile } from 'keycloak-js'; import { Button, Navbar, NavbarDivider, NavbarGroup, Popover, - PopoverPosition, Tooltip, + PopoverPosition, } from '@blueprintjs/core'; import * as actions from '../actions'; import { State } from '../state'; import cateIcon from '../resources/cate-icon-128.png'; -import UserMenu from './UserMenu'; import WorkspacesMenu from './WorkspacesMenu'; import FilesMenu from './FilesMenu'; import HelpMenu from './HelpMenu'; @@ -27,23 +24,17 @@ interface IDispatch { } interface IAppBarProps { - userProfile: KeycloakProfile | null, - pwaInstallPromotionVisible: boolean; } // noinspection JSUnusedLocalSymbols function mapStateToProps(state: State): IAppBarProps { return { - userProfile: state.communication.userProfile, - pwaInstallPromotionVisible: state.control.pwaInstallPromotionVisible, }; } const _AppBar: React.FC = ( { - userProfile, - pwaInstallPromotionVisible, dispatch, } ) => { @@ -52,10 +43,6 @@ const _AppBar: React.FC = ( dispatch(actions.showPreferencesDialog()); }; - const handleShowPwaInstallPrompt = () => { - dispatch(actions.showPwaInstallPrompt() as any); - }; - return ( @@ -63,19 +50,6 @@ const _AppBar: React.FC = (

Cate - ESA CCI Toolbox

- {pwaInstallPromotionVisible && ( - - - - - )} } position={PopoverPosition.BOTTOM}> @@ -86,16 +60,6 @@ const _AppBar: React.FC = ( - {userProfile !== null && ( - } position={PopoverPosition.BOTTOM}> - }> - - - -
- setWebAPIServiceURL(event.target.value)} - placeholder="Service URL" - large={true} - rightElement={ -
- -
- - ); -}; - -const AppModePage = connect(mapStateToProps)(_AppModePage); -export default AppModePage; - -function isValidURL(value: string) { - try { - const url = new URL(value); - return url.protocol === 'http:' || url.protocol === 'https:'; - } catch (e) { - return false; - } -} \ No newline at end of file diff --git a/src/renderer/containers/AppRouter.tsx b/src/renderer/containers/AppRouter.tsx deleted file mode 100644 index a6e76e0..0000000 --- a/src/renderer/containers/AppRouter.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from 'react'; -import { connect } from 'react-redux'; -import { BrowserRouter as Router, Redirect, Route, Switch } from "react-router-dom"; - -import { State, HubStatus } from '../state'; -import AppMainPageForHub from '../containers/AppMainPageForHub'; -import AppMainPageForSA from './AppMainPageForSA'; -import AppModePage from './AppModePage'; - - -interface IAppRouterProps { - hubStatus: HubStatus | null; -} - -function mapStateToProps(state: State): IAppRouterProps { - return { - hubStatus: state.communication.hubStatus - }; -} - -const AppRouter: React.FC = ({hubStatus}) => { - return ( - - - - - - - - - - { - // It should read - // (hubStatus === null || hubStatus.status !== 'ok') - // but this will always bring us back to "/" after login :( - (hubStatus !== null && hubStatus.status !== 'ok') - ? () - : () - } - - - - ); -} - -export default connect(mapStateToProps)(AppRouter); diff --git a/src/renderer/containers/PreferencesDialog.tsx b/src/renderer/containers/PreferencesDialog.tsx index b6b46d8..d073cdd 100644 --- a/src/renderer/containers/PreferencesDialog.tsx +++ b/src/renderer/containers/PreferencesDialog.tsx @@ -22,7 +22,7 @@ import deepEqual from 'deep-equal'; import { ModalDialog } from '../components/ModalDialog'; import { showToast } from '../toast'; import { isDefined } from '../../common/types'; -import { CATE_APP_VERSION } from '../../config'; +import { CATE_APP_VERSION } from '../../version'; interface IPreferencesDialogProps { diff --git a/src/renderer/containers/UserMenu.tsx b/src/renderer/containers/UserMenu.tsx deleted file mode 100644 index 4e4fbbe..0000000 --- a/src/renderer/containers/UserMenu.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import { Menu, MenuItem } from '@blueprintjs/core'; -import { ReactKeycloakInjectedProps, withKeycloak } from '@react-keycloak/web'; -import * as React from 'react'; -import { connect, Dispatch } from "react-redux"; -import { useHistory } from 'react-router-dom'; - -import * as actions from '../actions'; -import { State } from '../state'; - -interface IDispatch { - dispatch: Dispatch; -} - -interface IUserMenuProps { -} - -// noinspection JSUnusedLocalSymbols -function mapStateToProps(state: State): IUserMenuProps { - return {}; -} - -// noinspection JSUnusedLocalSymbols -const _UserMenu: React.FC = ( - { - dispatch, - keycloak, - keycloakInitialized, - } -) => { - const history = useHistory(); - - const handleAccount = () => { - dispatch(actions.manageAccount(keycloak) as any); - }; - - const handleLogout = () => { - dispatch(actions.logout(keycloak, history) as any); - }; - - return ( -
- - - - -
- ); -} - -const UserMenu = connect(mapStateToProps)(withKeycloak<'native'>(_UserMenu)); -export default UserMenu; diff --git a/src/renderer/containers/VersionTags.tsx b/src/renderer/containers/VersionTags.tsx index b81ddbb..63ff700 100644 --- a/src/renderer/containers/VersionTags.tsx +++ b/src/renderer/containers/VersionTags.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { Intent, Tag } from '@blueprintjs/core'; -import { CATE_APP_VERSION } from '../../config'; +import { CATE_APP_VERSION } from '../../version'; import { State } from '../state'; diff --git a/src/renderer/containers/WebAPIStatusBox.tsx b/src/renderer/containers/WebAPIStatusBox.tsx index 0594778..28b746e 100644 --- a/src/renderer/containers/WebAPIStatusBox.tsx +++ b/src/renderer/containers/WebAPIStatusBox.tsx @@ -1,13 +1,8 @@ import * as React from 'react'; import { connect, Dispatch } from 'react-redux'; -import { useHistory } from 'react-router-dom'; -import { Button, Classes, Dialog, Icon, IconName, Intent, ProgressBar, Spinner } from '@blueprintjs/core'; -import * as actions from '../actions'; -import MessageDetails from '../components/MessageDetails'; +import { Button, Classes, Dialog, Icon, IconName, Intent, ProgressBar } from '@blueprintjs/core'; import { State, WebAPIServiceInfo, WebAPIStatus } from '../state'; -import { showToast } from '../toast'; -import { PodStatus, ServiceProvisionAPI } from "../webapi/apis/ServiceProvisionAPI"; interface IDispatch { @@ -24,7 +19,6 @@ function mapStateToProps(state: State): IWebAPIStatusBoxProps { return { webAPIStatus: state.communication.webAPIStatus, webAPIServiceInfo: state.communication.webAPIServiceInfo, - username: state.communication.userProfile?.username || null }; } @@ -32,18 +26,12 @@ const _WebAPIStatusBox: React.FC = ( { webAPIStatus, webAPIServiceInfo, - username }) => { - const history = useHistory(); const reload = () => { window.location.reload(); }; - const goHome = () => { - history.replace("/"); - }; - switch (webAPIStatus) { case 'open': return null; @@ -89,7 +77,6 @@ const _WebAPIStatusBox: React.FC = ( icon="offline" isWaiting={false} onRetry={reload} - onCancel={goHome} />); } else { message = 'The connection to the Cate service has been closed unexpectedly.'; @@ -97,8 +84,6 @@ const _WebAPIStatusBox: React.FC = ( message={message} icon="offline" onRetry={reload} - onCancel={goHome} - username={username} />); } case 'error': @@ -106,15 +91,11 @@ const _WebAPIStatusBox: React.FC = ( message={'Oops! An error occurred while launching or connecting the Cate service.'} icon="offline" onRetry={reload} - onCancel={goHome} - username={username} />); default: return (); } }; @@ -190,101 +171,13 @@ const ErrorBox: React.FC = ( isWaiting, onRetry, onCancel, - username, } ) => { - const extendedMessage = ( - <> - {message} - {username && ( - - )} - - ); return (); }; - -////////////////////////////////////////////////////////////////////////////// - -type PodStatusState = 'init' | 'loading' | 'success' | 'error'; -const REQUEST_POD_STATUS_AFTER = 3; // seconds - -interface IPodStatusMessageProps { - username: string; - hint?: React.ReactNode; -} - -const PodStatusMessage: React.FC = ( - { - username, - }) => { - const [podStatusState, setPodStatusState] = React.useState('init'); - const [podStatus, setPodStatus] = React.useState(null); - React.useEffect(() => { - if (username && podStatusState === 'init') { - setPodStatusState('loading'); - window.setTimeout(() => { - new ServiceProvisionAPI().getPodStatus(username).then(podStatus => { - setPodStatusState('success'); - setPodStatus(podStatus); - }).catch(() => { - setPodStatusState('error'); - }); - }, REQUEST_POD_STATUS_AFTER * 1000); - } - }, [username, podStatusState]); - - let extraMessage = null; - if (podStatusState === 'init' || podStatusState === 'loading') { - extraMessage = (
Fetching container status... 
); - } else if (podStatusState === 'error') { - extraMessage = 'Failed fetching container status.'; - } else if (podStatusState === 'success' && podStatus) { - extraMessage = 'Container error.'; - const containerStatuses = podStatus?.container_statuses; - if (containerStatuses && containerStatuses.length > 0) { - const terminatedState = containerStatuses[0].last_state?.terminated - || containerStatuses[0].state?.terminated; - if (terminatedState) { - const terminationReason = terminatedState.reason; - const terminationExitCode = terminatedState.exit_code; - extraMessage = `Container terminated. Reason: ${terminationReason}, exit code ${terminationExitCode}.`; - } - } - } - - if (!podStatus) { - return (
{extraMessage}
); - } - - function handleCopyDetails(details: string) { - actions.copyTextToClipboard(details); - showToast({type: 'info', text: 'Details copied to clipboard.'}); - } - - return ( - <> -
{extraMessage}
- -
- This error typically occurs if our cloud resources are depleted. - This is likely due to high server loads caused by running demanding tasks - and/or multiple users competing for resources.
- To mitigate loss of data and time, we recommend that you save your workspace - often and enable the setting Reopen last workspace on startup in - the preferences. -
- - ); -}; diff --git a/src/renderer/initial-state.ts b/src/renderer/initial-state.ts index 4130edc..279c21b 100644 --- a/src/renderer/initial-state.ts +++ b/src/renderer/initial-state.ts @@ -63,9 +63,6 @@ export const INITIAL_CONTROL_STATE: ControlState = { newPlacemarkToolType: 'NoTool', entityUpdateCount: 0, - - pwaInstallPromotionVisible: false, - pwaDisplayMode: 'browser', }; @@ -143,8 +140,6 @@ export const INITIAL_COMMUNICATION_STATE: CommunicationState = { webAPIStatus: null, webAPIServiceInfo: null, webAPIClient: null, - hubStatus: null, - userProfile: null, tasks: {}, }; diff --git a/src/renderer/main.tsx b/src/renderer/main.tsx index e1b1665..70a72b2 100644 --- a/src/renderer/main.tsx +++ b/src/renderer/main.tsx @@ -4,24 +4,15 @@ import { Provider as StoreProvider } from 'react-redux'; import { applyMiddleware, createStore, Middleware, Store } from 'redux'; import { createLogger } from 'redux-logger'; import thunkMiddleware from 'redux-thunk'; -import Keycloak, { KeycloakInitOptions, KeycloakConfig } from 'keycloak-js' -import { KeycloakProvider } from '@react-keycloak/web' import * as actions from './actions' -import AppRouter from './containers/AppRouter'; import { stateReducer } from './reducers'; import { State } from './state'; -import { isElectron } from './electron'; -import { CONFIG } from '../config'; +import AppMainPageWrapper from './containers/AppMainPageWrapper'; +import { connectWebAPIService } from './actions'; +import { DEFAULT_SERVICE_URL } from './initial-state'; -const keycloak = Keycloak(getKeycloakConfig()); - -const keycloakProviderInitConfig: KeycloakInitOptions = { - onLoad: 'check-sso', - enableLogging: true, -}; - export function main() { const middlewares: Middleware[] = [thunkMiddleware]; @@ -46,57 +37,19 @@ export function main() { const middleware = applyMiddleware(...middlewares); const store = createStore(stateReducer, middleware) as Store; - const onKeycloakEvent = (event, error) => { - console.debug('onKeycloakEvent', event, error); + const search = new URLSearchParams(window.location.search); + let serviceUrl = DEFAULT_SERVICE_URL; + if (search.has("serviceUrl")) { + serviceUrl = search.get("serviceUrl"); } - - const onKeycloakTokens = (tokens) => { - console.debug('onKeycloakTokens', tokens); - } - - // Fetch hub status from GitHub - const deployment = CONFIG.webApi.endpointUrl.includes('stage') || CONFIG.webApi.endpointUrl.includes('dev') ? 'development' : 'production'; - fetch(`https://raw.githubusercontent.com/CCI-Tools/cate-status/main/${deployment}.json`, - {mode: 'cors'}) - .then(response => - response.json()) - .then(hubStatus => - store.dispatch(actions.updateHubStatus( - {...hubStatus, deployment}))) - .catch(e => console.error(e)); + store.dispatch(connectWebAPIService(serviceUrl) as any); ReactDOM.render( ( - - - - - + + + ), document.getElementById('root') ); - - if (!isElectron()) { - // - // Desktop-PWA app install, see https://web.dev/customize-install/ - // - window.addEventListener('beforeinstallprompt', (event: Event) => { - // Update UI notify the user they can install the PWA - store.dispatch(actions.showPwaInstallPromotion(event)); - console.log('BEFORE INSTALL PROMPT:', event); - }); - } -} - -function getKeycloakConfig(): KeycloakConfig { - const {url, realm, clientId} = CONFIG.auth; - if (!realm || !url || !clientId) { - throw new Error('Missing or incomplete KeyCloak configuration'); - } - return {realm, url, clientId}; } diff --git a/src/renderer/reducers.ts b/src/renderer/reducers.ts index e03286d..40c2af1 100644 --- a/src/renderer/reducers.ts +++ b/src/renderer/reducers.ts @@ -1,5 +1,4 @@ import { combineReducers, Reducer } from 'redux'; -import { KeycloakProfile } from 'keycloak-js'; import deepEqual from 'deep-equal'; import * as assert from '../common/assert'; @@ -318,15 +317,6 @@ const controlReducer = (state: ControlState = INITIAL_CONTROL_STATE, action: Act case actions.UPDATE_MOUSE_IDLE_STATE: { return {...state, ...action.payload}; } - case actions.SHOW_PWA_INSTALL_PROMOTION: { - return {...state, pwaInstallPromotionVisible: true}; - } - case actions.HIDE_PWA_INSTALL_PROMOTION: { - return {...state, pwaInstallPromotionVisible: false}; - } - case actions.UPDATE_PWA_DISPLAY_MODE: { - return {...state, pwaDisplayMode: action.payload}; - } case actions.OPEN_MESSAGE_BOX: { return { ...state, messageBox: {...state.messageBox, ...action.payload, isOpen: true} @@ -886,12 +876,6 @@ const communicationReducer = (state: CommunicationState = INITIAL_COMMUNICATION_ delete tasks[action.payload.jobId]; return {...state, tasks}; } - case actions.SET_USER_PROFILE: { - return { - ...state, - userProfile: action.payload as KeycloakProfile, - }; - } } return state; }; diff --git a/src/renderer/state.ts b/src/renderer/state.ts index 563799a..fa265eb 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -1,7 +1,6 @@ import { IconName } from '@blueprintjs/core'; import { Feature, FeatureCollection, GeoJsonObject, Point } from 'geojson'; import { JSONSchema7 } from 'json-schema'; -import { KeycloakProfile } from 'keycloak-js'; import { SimpleStyle } from '../common/geojson-simple-style'; import { HostOS } from '../common/paths'; @@ -55,7 +54,6 @@ export interface DataState { hasWebGL: boolean; } -export type WebAPIProvision = 'standAlone' | 'hub'; export type WebAPIStatus = 'login' | 'launching' | 'connecting' | 'open' | 'error' | 'closed' | 'shuttingDown' | 'loggingOut'; @@ -750,8 +748,6 @@ export interface CommunicationState { webAPIServiceInfo: WebAPIServiceInfo | null; webAPIStatus: WebAPIStatus | null; webAPIClient: WebAPIClient | null; - userProfile: KeycloakProfile | null; - hubStatus: HubStatus | null; // A map that stores the current state of any tasks (e.g. data fetch jobs from remote API) given a jobId tasks: { [jobId: number]: TaskState; }; } @@ -806,9 +802,6 @@ export interface ControlState { // Used to force component update after an entity's properties have changed entityUpdateCount: number; - - pwaInstallPromotionVisible: boolean; - pwaDisplayMode: 'standalone' | 'browser'; } export interface DialogState { diff --git a/src/renderer/webapi/apis/ServiceProvisionAPI.ts b/src/renderer/webapi/apis/ServiceProvisionAPI.ts deleted file mode 100644 index 6b34b21..0000000 --- a/src/renderer/webapi/apis/ServiceProvisionAPI.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { HttpError } from '../HttpError'; -import { CONFIG } from '../../../config'; - -export interface Condition { - type: string; - status: string; - reason: string | null; - message: string | null; - last_probe_time: string | null; - last_transition_time: string; -} - -export interface ContainerStateValue { - container_id?: string; - exit_code?: number; - finished_at?: string; - message?: string | null; - reason?: string; - signal?: string | null; - started_at?: string; -} - -export interface ContainerState { - running: ContainerStateValue | null; - terminated: ContainerStateValue | null; - waiting: ContainerStateValue | null; -} - -export interface ContainerStatus { - name: string; - ready: string; - restart_count: number; - started: boolean; - image: string; - image_id: string; - container_id: string; - state: ContainerState; - last_state: ContainerState; -} - -export interface PodStatus { - phase: string; - host_ip?: string; - start_time?: string; - message?: string | null; - nominated_node_name?: string | null; - pod_ip?: string; - qos_class?: string; - reason?: string | null; - pod_i_ps?: Array<{ ip: string }>; - - conditions?: Condition[]; - container_statuses?: ContainerStatus[]; - init_container_statuses?: null | ContainerStatus[]; - ephemeral_container_statuses?: null | ContainerStatus[]; -} - -interface StartResult { - serverUrl: string; -} - -interface CountResult { - running_pods: number; -} - -/** - * Represents the cate-hub API. - */ -export class ServiceProvisionAPI { - - /** - * Get status of the user's webapi (RUNNING, PENDING, FAILED, ...). - * See https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/ - */ - async getPodStatus(username: string, token?: string): Promise { - return await this.performServiceOp(CONFIG.webApi.managApiUrl, - 'GET', username, token); - } - - async startService(username: string, token: string): Promise { - const result = await this.performServiceOp(CONFIG.webApi.managApiUrl, - 'POST', username, token); - return result.serverUrl; - } - - async stopServiceInstance(username: string): Promise { - return await this.performServiceOp(CONFIG.webApi.closeApiUrl, - 'DELETE', username); - } - - async getServiceCount(): Promise { - const result = await this.performServiceOp(CONFIG.webApi.countApiUrl, - 'GET'); - return result.running_pods as number; - } - - // noinspection JSMethodCanBeStatic - private async performServiceOp(urlPattern: string, - method: 'GET' | 'POST' | 'DELETE', - username?: string, - token?: string): Promise { - - let headers = [['Accept', `application/json`]]; - if (token) { - headers = [ - ...headers, - ['Authorization', `Bearer ${token}`], - ]; - } - - let url = urlPattern; - if (urlPattern.indexOf('{username}') > 0) { - url = new URL(urlPattern.replace('{username}', username)).toString() - } - - const response: Response = await fetch(url, {method, headers, mode: 'cors'}); - if (!response.ok) { - throw HttpError.fromResponse(response); - } - return response.json(); - } -} - diff --git a/src/serviceWorker.ts b/src/serviceWorker.ts deleted file mode 100644 index 244e27c..0000000 --- a/src/serviceWorker.ts +++ /dev/null @@ -1,166 +0,0 @@ -// This optional code is used to register a service worker. -// register() is not called by default. - -// This lets the app load faster on subsequent visits in production, and gives -// it offline capabilities. However, it also means that developers (and users) -// will only see deployed updates on subsequent visits to a page, after all the -// existing tabs open on the page have been closed, since previously cached -// resources are updated in the background. - -// To learn more about the benefits of this model and instructions on how to -// opt-in, read https://bit.ly/CRA-PWA - -// The app cache is only updated, if this file changes (i.e. the compiled -// "./service-worker.js" file changes). Therefore we use a version number here, -// so we can force updates. -// -const CATE_PWA_VERSION = "3.1.5-dev.0"; - -console.debug(`Cate PWA version ${CATE_PWA_VERSION}`); - -const isLocalhost = Boolean( - window.location.hostname === 'localhost' || - // [::1] is the IPv6 localhost address. - window.location.hostname === '[::1]' || - // 127.0.0.0/8 are considered localhost for IPv4. - window.location.hostname.match( - /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ - ) -); - -type Config = { - onSuccess?: (registration: ServiceWorkerRegistration) => void; - onUpdate?: (registration: ServiceWorkerRegistration) => void; - onError?: (error: Error) => void; -}; - -export function register(config?: Config) { - if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL( - process.env.PUBLIC_URL, - window.location.href - ); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return; - } - - window.addEventListener('load', () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config); - - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - 'This web app is being served cache-first by a service ' + - 'worker. To learn more, visit https://create-react-app.dev/docs/making-a-progressive-web-app/' - ); - }); - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config); - } - }); - } -} - -function registerValidSW(swUrl: string, config?: Config) { - navigator.serviceWorker - .register(swUrl) - .then(registration => { - registration.onupdatefound = () => { - const installingWorker = registration.installing; - if (installingWorker == null) { - return; - } - installingWorker.onstatechange = () => { - if (installingWorker.state === 'installed') { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log( - 'New content is available and will be used when all ' + - 'tabs for this page are closed. ' + - 'See https://create-react-app.dev/docs/making-a-progressive-web-app/.' - ); - - // Execute callback - if (config && config.onUpdate) { - config.onUpdate(registration); - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log('Content is cached for offline use.'); - - // Execute callback - if (config && config.onSuccess) { - config.onSuccess(registration); - } - } - } - }; - }; - }) - .catch(error => { - console.error('Error during service worker registration:', error); - // Execute callback - if (config && config.onError) { - config.onError(error); - } - }); -} - -function checkValidServiceWorker(swUrl: string, config?: Config) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl, { - headers: {'Service-Worker': 'script'} - }) - .then(response => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get('content-type'); - if ( - response.status === 404 || - (contentType != null && contentType.indexOf('javascript') === -1) - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config); - } - }) - .catch(() => { - console.log( - 'No internet connection found. App is running in offline mode.' - ); - }); -} - -// noinspection JSUnusedGlobalSymbols -export function unregister() { - if ('serviceWorker' in navigator) { - navigator.serviceWorker.ready - .then(registration => { - // noinspection JSIgnoredPromiseFromCall - registration.unregister(); - }) - .catch(error => { - console.error(error.message); - - }); - } -} diff --git a/src/version.ts b/src/version.ts new file mode 100644 index 0000000..f8037ae --- /dev/null +++ b/src/version.ts @@ -0,0 +1,5 @@ +// IMPORTANT: Any changes of CATE_APP_VERSION must be synchronized +// with the version field in "../package.json". +// +export const CATE_APP_VERSION = "3.1.5-dev.0"; + diff --git a/yarn.lock b/yarn.lock index 9ae099f..e48db33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1110,7 +1110,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.0": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4": version "7.11.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== @@ -1445,25 +1445,6 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== -"@react-keycloak/core@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@react-keycloak/core/-/core-1.2.1.tgz#aca9990bda4ff2b3796e0193cb97a133e8d66f2d" - integrity sha512-ekXRKaAoeimbWENYn6xJkzd+c5kxPNE7HOSc2GdmDTJ2Krk1+NJQ+x+wNbVSFvBABhhkFUxH3w452Zg21EYLOg== - dependencies: - "@babel/runtime" "^7.9.0" - prop-types "^15.7.2" - react-fast-compare "^3.0.1" - -"@react-keycloak/web@^1.0.6": - version "1.0.6" - resolved "https://registry.yarnpkg.com/@react-keycloak/web/-/web-1.0.6.tgz#4c15411badf886b2af0adc89d559a25c700128b7" - integrity sha512-LI/Sc/Bqc5kfRdgS0VdeKAZ19RMxLzYNLNPlh4pZXS5vBNuYSKxm3U7R7Z1gtdg+Xdd3RIMRhK67IVwbs8XlEA== - dependencies: - "@babel/runtime" "^7.9.0" - "@react-keycloak/core" "^1.2.1" - hoist-non-react-statics "^3.3.2" - prop-types "^15.7.2" - "@sheerun/mutationobserver-shim@^0.3.2": version "0.3.3" resolved "https://registry.yarnpkg.com/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#5405ee8e444ed212db44e79351f0c70a582aae25" @@ -1737,11 +1718,6 @@ dependencies: "@types/unist" "*" -"@types/history@*": - version "4.7.7" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.7.tgz#613957d900fab9ff84c8dfb24fa3eef0c2a40896" - integrity sha512-2xtoL22/3Mv6a70i4+4RB7VgbDDORoWwjcqeNysojZA0R7NK17RbY5Gof/2QiFfJgX+KkWghbwJ+d/2SB8Ndzg== - "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.3" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz#4ba8ddb720221f432e443bd5f9117fd22cfd4762" @@ -1868,23 +1844,6 @@ "@types/react" "*" redux "^3.6.0" -"@types/react-router-dom@^5.1.5": - version "5.1.5" - resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.1.5.tgz#7c334a2ea785dbad2b2dcdd83d2cf3d9973da090" - integrity sha512-ArBM4B1g3BWLGbaGvwBGO75GNFbLDUthrDojV2vHLih/Tq8M+tgvY1DSwkuNrPSwdp/GUL93WSEpTZs8nVyJLw== - dependencies: - "@types/history" "*" - "@types/react" "*" - "@types/react-router" "*" - -"@types/react-router@*": - version "5.1.8" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.8.tgz#4614e5ba7559657438e17766bb95ef6ed6acc3fa" - integrity sha512-HzOyJb+wFmyEhyfp4D4NYrumi+LQgQL/68HvJO+q6XtuHSDvw6Aqov7sCAhjbNq3bUPgPqbdvjXC5HeB2oEAPg== - dependencies: - "@types/history" "*" - "@types/react" "*" - "@types/react-test-renderer@^16.8.1": version "16.9.3" resolved "https://registry.yarnpkg.com/@types/react-test-renderer/-/react-test-renderer-16.9.3.tgz#96bab1860904366f4e848b739ba0e2f67bcae87e" @@ -2785,7 +2744,7 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= -base64-js@1.3.1, base64-js@^1.0.2: +base64-js@^1.0.2: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== @@ -6013,18 +5972,6 @@ hex-color-regex@^1.1.0: resolved "https://registry.yarnpkg.com/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e" integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ== -history@^4.9.0: - version "4.10.1" - resolved "https://registry.yarnpkg.com/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3" - integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew== - dependencies: - "@babel/runtime" "^7.1.2" - loose-envify "^1.2.0" - resolve-pathname "^3.0.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - value-equal "^1.0.1" - hmac-drbg@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" @@ -6034,7 +5981,7 @@ hmac-drbg@^1.0.0: minimalistic-assert "^1.0.0" minimalistic-crypto-utils "^1.0.1" -hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7283,11 +7230,6 @@ js-cookie@^2.2.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== -js-sha256@0.9.0: - version "0.9.0" - resolved "https://registry.yarnpkg.com/js-sha256/-/js-sha256-0.9.0.tgz#0b89ac166583e91ef9123644bd3c5334ce9d0966" - integrity sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -7503,14 +7445,6 @@ jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3: array-includes "^3.1.1" object.assign "^4.1.0" -keycloak-js@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/keycloak-js/-/keycloak-js-8.0.2.tgz#2af5868aa4313c111601115e990ba317e5f14489" - integrity sha512-5Jy5rHx0oURTtj2T6LSMLiqyHzDzPr6ZnaY1fwCBlMsiMx7RVsIBeysYZ5yy29ojH9IMzNJzlZvuLKbEOCmgDQ== - dependencies: - base64-js "1.3.1" - js-sha256 "0.9.0" - killable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892" @@ -7764,7 +7698,7 @@ longest-streak@^2.0.0: resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4" integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg== -loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -8194,14 +8128,6 @@ min-indent@^1.0.0: resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== -mini-create-react-context@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/mini-create-react-context/-/mini-create-react-context-0.4.0.tgz#df60501c83151db69e28eac0ef08b4002efab040" - integrity sha512-b0TytUgFSbgFJGzJqXPKCFCBWigAjpjo+Fl7Vf7ZbKRDptszpppKxXH6DRXEABZ/gcEQczeb0iZ7JvL8e8jjCA== - dependencies: - "@babel/runtime" "^7.5.5" - tiny-warning "^1.0.3" - mini-css-extract-plugin@0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-0.9.0.tgz#47f2cf07aa165ab35733b1fc97d4c46c0564339e" @@ -9103,13 +9029,6 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== -path-to-regexp@^1.7.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" - integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== - dependencies: - isarray "0.0.1" - path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -10299,11 +10218,6 @@ react-error-overlay@^6.0.7: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108" integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA== -react-fast-compare@^3.0.1: - version "3.2.0" - resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" - integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== - react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -10364,35 +10278,6 @@ react-redux@^5.1.2: react-is "^16.6.0" react-lifecycles-compat "^3.0.0" -react-router-dom@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-5.2.0.tgz#9e65a4d0c45e13289e66c7b17c7e175d0ea15662" - integrity sha512-gxAmfylo2QUjcwxI63RhQ5G85Qqt4voZpUXSEqCwykV0baaOTQDR1f0PmY8AELqIyVc0NEZUj0Gov5lNGcXgsA== - dependencies: - "@babel/runtime" "^7.1.2" - history "^4.9.0" - loose-envify "^1.3.1" - prop-types "^15.6.2" - react-router "5.2.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - -react-router@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-5.2.0.tgz#424e75641ca8747fbf76e5ecca69781aa37ea293" - integrity sha512-smz1DUuFHRKdcJC0jobGo8cVbhO3x50tCL4icacOlcwDOEQPq4TMqwx3sY1TP+DvtTgz4nm3thuo7A+BK2U0Dw== - dependencies: - "@babel/runtime" "^7.1.2" - history "^4.9.0" - hoist-non-react-statics "^3.1.0" - loose-envify "^1.3.1" - mini-create-react-context "^0.4.0" - path-to-regexp "^1.7.0" - prop-types "^15.6.2" - react-is "^16.6.0" - tiny-invariant "^1.0.2" - tiny-warning "^1.0.0" - react-scripts@3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.4.1.tgz#f551298b5c71985cc491b9acf3c8e8c0ae3ada0a" @@ -10902,11 +10787,6 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-pathname@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd" - integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng== - resolve-url-loader@3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz#28931895fa1eab9be0647d3b2958c100ae3c0bf0" @@ -12118,16 +11998,6 @@ timsort@^0.3.0: resolved "https://registry.yarnpkg.com/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4" integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= -tiny-invariant@^1.0.2: - version "1.1.0" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875" - integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw== - -tiny-warning@^1.0.0, tiny-warning@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" - integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== - tinycolor2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.1.tgz#f4fad333447bc0b07d4dc8e9209d8f39a8ac77e8" @@ -12618,11 +12488,6 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" -value-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c" - integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw== - vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From 6a7e31ea3dd4e7dd2659763e5276b52af55c8bf3 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Mon, 20 Mar 2023 17:43:07 +0100 Subject: [PATCH 04/20] Started 4.0.0 --- CHANGES.md | 17 +++++++-- package.json | 2 +- src/renderer/actions.ts | 13 ++++--- src/renderer/containers/DataSourcesPanel.tsx | 40 +++++++++++++------- src/version.ts | 2 +- 5 files changed, 51 insertions(+), 23 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7208793..745b490 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,17 @@ -### Changes 3.1.5 (in development) - - +### Changes 4.0.0 (in development) + +* Since Cate App is now designed to work inside of Jupyter Lab and standalone, + all code dealing with user login and Cate service provisioning in the + cloud has been removed. + +* The following changes apply to the "local" data store because the server + now serves local files from the current working directory. This has been + done to let Cate integrated with Jupyter Lab pickj up all datasets found + in the Lab's workspace. + - Users can now refresh the list of diles from the "local" data store. + - Users can no longer add/remove data sources to the "local" data store. + This is a temporary change. We may reassign add/remove actions to perform + a filtering on the local datasets. ### Changes 3.1.4 diff --git a/package.json b/package.json index 574b31e..194f36c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cate-app", - "version": "3.1.5-dev.0", + "version": "4.0.0-dev.0", "private": true, "homepage": ".", "dependencies": { diff --git a/src/renderer/actions.ts b/src/renderer/actions.ts index ebf1544..9e55c5c 100644 --- a/src/renderer/actions.ts +++ b/src/renderer/actions.ts @@ -896,21 +896,24 @@ export function updateDataSources(dataStoreId: string, dataSources): Action { return {type: UPDATE_DATA_SOURCES, payload: {dataStoreId, dataSources}}; } -export function setSelectedDataStoreId(selectedDataStoreId: string | null): ThunkAction { +export function setSelectedDataStoreId(selectedDataStoreId: string | null, force: boolean = false): ThunkAction { return (dispatch: Dispatch, getState: GetState) => { - if (getState().session.selectedDataStoreId === selectedDataStoreId) { - //return; - } dispatch(setSelectedDataStoreIdImpl(selectedDataStoreId)); if (selectedDataStoreId !== null) { const dataStore = getState().data.dataStores.find(dataStore => dataStore.id === selectedDataStoreId); - if (!dataStore.dataSources) { + if (force || !dataStore.dataSources) { dispatch(loadDataSources(selectedDataStoreId, true)); } } } } +export function refreshLocalDataStore(): ThunkAction { + return (dispatch: Dispatch) => { + dispatch(setSelectedDataStoreId("local", true)); + } +} + export function setSelectedDataStoreIdImpl(selectedDataStoreId: string | null) { return updateSessionState({selectedDataStoreId}); } diff --git a/src/renderer/containers/DataSourcesPanel.tsx b/src/renderer/containers/DataSourcesPanel.tsx index 402a7ba..ed4f153 100644 --- a/src/renderer/containers/DataSourcesPanel.tsx +++ b/src/renderer/containers/DataSourcesPanel.tsx @@ -96,6 +96,8 @@ interface IDataSourcesPanelDispatch { updateSessionState(sessionState: any): void; + refreshLocalDataStore(): void; + loadDataSourceMetaInfo(dataStoreId: string, dataSourceId: string): void; showDialog(dialogId: string): void; @@ -110,6 +112,7 @@ const mapDispatchToProps = { setSessionState: actions.setSessionProperty, setControlState: actions.setControlProperty, updateSessionState: actions.updateSessionState, + refreshLocalDataStore: actions.refreshLocalDataStore, showDialog: actions.showDialog, hideDialog: actions.hideDialog, }; @@ -133,6 +136,7 @@ class DataSourcesPanel extends React.Component + {/*{isLocalStore && }*/} + {/*{isLocalStore && }*/} {isLocalStore && } - {isLocalStore && } {isNotVerified && ( @@ -257,6 +267,10 @@ class DataSourcesPanel extends React.Component Date: Mon, 20 Mar 2023 17:49:28 +0100 Subject: [PATCH 05/20] React upgrade --- package.json | 4 ++-- yarn.lock | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 194f36c..721bf9e 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,11 @@ "deep-object-diff": "^1.1.0", "js-cookie": "^2.2.1", "normalize.css": "^8.0.1", - "react": "^16.13.1", + "react": "^16.14.0", "react-ace": "^5.9.0", "react-addons-css-transition-group": "^15.6.2", "react-color": "^2.17.3", - "react-dom": "^16.13.1", + "react-dom": "^16.14.0", "react-dropzone": "^11.0.2", "react-markdown": "^6.0.2", "react-redux": "^5.1.2", diff --git a/yarn.lock b/yarn.lock index e48db33..45cf4df 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10194,10 +10194,10 @@ react-dev-utils@^10.2.1: strip-ansi "6.0.0" text-table "0.2.0" -react-dom@^16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f" - integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag== +react-dom@^16.14.0: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.14.0.tgz#7ad838ec29a777fb3c75c3a190f661cf92ab8b89" + integrity sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" @@ -10369,10 +10369,10 @@ react-transition-group@^2.9.0: prop-types "^15.6.2" react-lifecycles-compat "^3.0.4" -react@^16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e" - integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w== +react@^16.14.0: + version "16.14.0" + resolved "https://registry.yarnpkg.com/react/-/react-16.14.0.tgz#94d776ddd0aaa37da3eda8fc5b6b18a4c9a3114d" + integrity sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g== dependencies: loose-envify "^1.1.0" object-assign "^4.1.1" From dbdb5b7e5e48bcdc241fda3c7d36c9e61783f281 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sat, 25 Mar 2023 17:56:23 +0100 Subject: [PATCH 06/20] Now using workspace identifiers --- CHANGES.md | 7 ++- package.json | 2 +- src/renderer/actions.ts | 12 ++-- src/renderer/containers/FigureView.tsx | 15 +++-- src/renderer/containers/GlobeView.tsx | 4 +- .../containers/globe-view-layers.spec.ts | 6 +- src/renderer/containers/globe-view-layers.ts | 14 ++--- src/renderer/selectors.ts | 3 + src/renderer/state-util.ts | 61 ++++++++++++------- src/renderer/state.ts | 8 ++- src/renderer/webapi/apis/WorkspaceAPI.ts | 1 + 11 files changed, 83 insertions(+), 50 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7208793..75cf81c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ -### Changes 3.1.5 (in development) - +### Changes 4.0.0 (in development) +* Now using workspace identifiers instead of base directories in resource + URLs of the WebAPI. This way we no longer need to URL-encode workspace + directories in WebAPI URLs, which did not work with + [jupyter-server-proxy](https://jupyter-server-proxy.readthedocs.io/). ### Changes 3.1.4 diff --git a/package.json b/package.json index 574b31e..194f36c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cate-app", - "version": "3.1.5-dev.0", + "version": "4.0.0-dev.0", "private": true, "homepage": ".", "dependencies": { diff --git a/src/renderer/actions.ts b/src/renderer/actions.ts index ebf1544..9ef71f2 100644 --- a/src/renderer/actions.ts +++ b/src/renderer/actions.ts @@ -1947,8 +1947,8 @@ export function notifySelectedEntityChange(viewId: string, layer: LayerState | n const resId = selectedEntity['_resId']; const featureIndex = +selectedEntity['_idx']; const baseUrl = selectors.webAPIRestUrlSelector(getState()); - const baseDir = workspace.baseDir; - const featureUrl = getFeatureUrl(baseUrl, baseDir, {resId}, featureIndex); + const workspaceId = workspace.id; + const featureUrl = getFeatureUrl(baseUrl, workspaceId, {resId}, featureIndex); reloadEntityWithOriginalGeometry(selectedEntity, featureUrl, (layer as any).style); } } @@ -2013,10 +2013,10 @@ export function updateTableViewData(viewId: string, export function loadTableViewData(viewId: string, resName: string, varName: string | null): ThunkAction { return (dispatch: Dispatch, getState: GetState) => { const restUrl = selectors.webAPIRestUrlSelector(getState()); - const baseDir = selectors.workspaceBaseDirSelector(getState()); + const workspaceId = selectors.workspaceIdSelector(getState()); const resource = selectors.resourcesSelector(getState()).find(res => res.name === resName); if (resource) { - const csvUrl = getCsvUrl(restUrl, baseDir, {resId: resource.id}, varName); + const csvUrl = getCsvUrl(restUrl, workspaceId, {resId: resource.id}, varName); dispatch(updateTableViewData(viewId, resName, varName, null, null, true)); d3.csv(csvUrl) .then((dataRows: any[]) => { @@ -2037,8 +2037,8 @@ export const UPDATE_ANIMATION_VIEW_DATA = 'UPDATE_ANIMATION_VIEW_DATA'; export function loadAnimationViewData(viewId: string, resId: number): ThunkAction { return (dispatch: Dispatch, getState: GetState) => { const restUrl = selectors.webAPIRestUrlSelector(getState()); - const baseDir = selectors.workspaceBaseDirSelector(getState()); - const htmlUrl = getHtmlUrl(restUrl, baseDir, resId); + const workspaceId = selectors.workspaceIdSelector(getState()); + const htmlUrl = getHtmlUrl(restUrl, workspaceId, resId); const xmlHttp = new XMLHttpRequest(); xmlHttp.onreadystatechange = () => { diff --git a/src/renderer/containers/FigureView.tsx b/src/renderer/containers/FigureView.tsx index 139d841..c6d63c9 100644 --- a/src/renderer/containers/FigureView.tsx +++ b/src/renderer/containers/FigureView.tsx @@ -15,7 +15,7 @@ interface IFigureViewOwnProps { interface IFigureViewProps extends IFigureViewOwnProps { baseUrl: string; - baseDir: string | null; + workspaceId: number | null; figureResources: ResourceState[]; mplWebSocketUrl: string; } @@ -24,7 +24,7 @@ function mapStateToProps(state: State, ownProps: IFigureViewOwnProps): IFigureVi return { view: ownProps.view, baseUrl: selectors.webAPIRestUrlSelector(state), - baseDir: selectors.workspaceBaseDirSelector(state), + workspaceId: selectors.workspaceIdSelector(state), figureResources: selectors.figureResourcesSelector(state), mplWebSocketUrl: selectors.mplWebSocketUrlSelector(state), }; @@ -42,14 +42,15 @@ class FigureView extends React.Component, } onDownload(figureId: number) { - const imageBaseUrl = getMPLDownloadUrl(this.props.baseUrl, this.props.baseDir, figureId); + const imageBaseUrl = getMPLDownloadUrl(this.props.baseUrl, this.props.workspaceId, figureId); this.props.dispatch(actions.saveFigureImageAs(imageBaseUrl, figureId) as any); } render() { const view = this.props.view; + const workspaceId = this.props.workspaceId; const figureResource = this.getFigureResource(); - if (!figureResource) { + if (workspaceId === null || !figureResource) { return (Figure not found.); } const compId = `MplFigurePanel-${figureResource.id}-${view.id}`; @@ -60,7 +61,11 @@ class FigureView extends React.Component, figureId={figureResource.id} figureUpdateCount={figureResource.updateCount} figureName={figureResource.name} - webSocketUrl={getMPLWebSocketUrl(this.props.mplWebSocketUrl, this.props.baseDir, figureResource.id)} + webSocketUrl={getMPLWebSocketUrl( + this.props.mplWebSocketUrl, + workspaceId, + figureResource.id + )} onDownload={this.onDownload} /> ); diff --git a/src/renderer/containers/GlobeView.tsx b/src/renderer/containers/GlobeView.tsx index 07f3f5c..7001e74 100644 --- a/src/renderer/containers/GlobeView.tsx +++ b/src/renderer/containers/GlobeView.tsx @@ -268,8 +268,8 @@ class GlobeView extends React.Component { export const isScratchWorkspaceSelector = (state: State): boolean => { return state.data.workspace && state.data.workspace.isScratch; }; +export const workspaceIdSelector = (state: State): number | null => { + return state.data.workspace && state.data.workspace.id; +}; export const workspaceBaseDirSelector = (state: State): string | null => { return state.data.workspace && state.data.workspace.baseDir; }; diff --git a/src/renderer/state-util.ts b/src/renderer/state-util.ts index 53a6d28..a716945 100644 --- a/src/renderer/state-util.ts +++ b/src/renderer/state-util.ts @@ -195,8 +195,10 @@ function _checkDataSourceCapability(dataSource: DataSourceState, } } -export function getTileUrl(baseUrl: string, baseDir: string, layer: VariableImageLayerState): string { - return baseUrl + `ws/res/tile/${encodeURIComponent(baseDir)}/${layer.resId}/{z}/{y}/{x}.png?` +export function getTileUrl(baseUrl: string, + workspaceId: number, + layer: VariableImageLayerState): string { + return baseUrl + `ws/res/tile/${workspaceId}/${layer.resId}/{z}/{y}/{x}.png?` + `&var=${encodeURIComponent(layer.varName)}` + `&index=${encodeURIComponent((layer.varIndex || []).join())}` + `&cmap=${encodeURIComponent(layer.colorMapName) + (layer.alphaBlending ? '_alpha' : '')}` @@ -204,36 +206,43 @@ export function getTileUrl(baseUrl: string, baseDir: string, layer: VariableImag + `&max=${encodeURIComponent(layer.displayMax + '')}`; } -export function getFeatureCollectionUrl(baseUrl: string, baseDir: string, ref: ResourceRefState): string { - return baseUrl + `ws/res/geojson/${encodeURIComponent(baseDir)}/${ref.resId}`; +export function getFeatureCollectionUrl(baseUrl: string, + workspaceId: number, + ref: ResourceRefState): string { + return baseUrl + `ws/res/geojson/${workspaceId}/${ref.resId}`; } -export function getFeatureUrl(baseUrl: string, baseDir: string, ref: ResourceRefState, index: number): string { - return baseUrl + `ws/res/geojson/${encodeURIComponent(baseDir)}/${ref.resId}/${index}`; +export function getFeatureUrl(baseUrl: string, + workspaceId: number, + ref: ResourceRefState, + index: number): string { + return baseUrl + `ws/res/geojson/${workspaceId}/${ref.resId}/${index}`; } -export function getCsvUrl(baseUrl: string, baseDir: string, ref: ResourceRefState, varName?: string | null): string { +export function getCsvUrl(baseUrl: string, + workspaceId: number, + ref: ResourceRefState, varName?: string | null): string { let varPart = ''; if (varName) { varPart = `?var=${encodeURIComponent(varName)}`; } - return baseUrl + `ws/res/csv/${encodeURIComponent(baseDir)}/${ref.resId}${varPart}`; + return baseUrl + `ws/res/csv/${workspaceId}/${ref.resId}${varPart}`; } -export function getHtmlUrl(baseUrl: string, baseDir: string, resId: number): string { - return baseUrl + `ws/res/html/${encodeURIComponent(baseDir)}/${resId}`; +export function getHtmlUrl(baseUrl: string, workspaceId: number, resId: number): string { + return baseUrl + `ws/res/html/${workspaceId}/${resId}`; } export function getGeoJSONCountriesUrl(baseUrl: string): string { return baseUrl + 'ws/countries'; } -export function getMPLWebSocketUrl(baseUrl: string, baseDir: string, figureId: number): string { - return `${baseUrl}${encodeURIComponent(baseDir)}/${encodeURIComponent('' + figureId)}`; +export function getMPLWebSocketUrl(baseUrl: string, workspaceId: number, figureId: number): string { + return `${baseUrl}${workspaceId}/${figureId}`; } -export function getMPLDownloadUrl(baseUrl: string, baseDir: string, figureId: number): string { - return `${baseUrl}mpl/download/${encodeURIComponent(baseDir)}/${encodeURIComponent('' + figureId)}`; +export function getMPLDownloadUrl(baseUrl: string, workspaceId: number, figureId: number): string { + return `${baseUrl}mpl/download/${workspaceId}/${figureId}`; } export function isSpatialImageVariable(variable: VariableState): boolean { @@ -292,24 +301,29 @@ export function getLayerTypeIconName(layer: LayerState): IconName { return 'layer'; } -export function findResource(resources: ResourceState[], ref: ResourceRefState): ResourceState | null { +export function findResource(resources: ResourceState[], + ref: ResourceRefState): ResourceState | null { return findResourceById(resources, ref.resId); } -export function findResourceByName(resources: ResourceState[], name: string): ResourceState | null { +export function findResourceByName(resources: ResourceState[], + name: string): ResourceState | null { return resources.find(r => r.name === name); } -export function findResourceById(resources: ResourceState[], id: number): ResourceState | null { +export function findResourceById(resources: ResourceState[], + id: number): ResourceState | null { return resources.find(r => r.id === id); } -export function findVariable(resources: ResourceState[], ref: VariableRefState): VariableState | null { +export function findVariable(resources: ResourceState[], + ref: VariableRefState): VariableState | null { const resource = findResource(resources, ref); return resource && resource.variables && resource.variables.find(v => v.name === ref.varName); } -export function findOperation(operations: OperationState[], name: string): OperationState | null { +export function findOperation(operations: OperationState[], + name: string): OperationState | null { return operations && operations.find(op => op.qualifiedName === name || op.name === name); } @@ -327,7 +341,8 @@ export function findDataSource(dataStores: DataStoreState[], return null; } -export function findVariableIndexCoordinates(resources: ResourceState[], ref: VariableDataRefState): any[] { +export function findVariableIndexCoordinates(resources: ResourceState[], + ref: VariableDataRefState): any[] { const resource = findResourceById(resources, ref.resId); if (!resource) { return EMPTY_ARRAY; @@ -374,7 +389,8 @@ export function findVariableIndexCoordinates(resources: ResourceState[], ref: Va return coords; } -export function getNonSpatialIndexers(resource: ResourceState, ref: VariableDataRefState): DimSizes { +export function getNonSpatialIndexers(resource: ResourceState, + ref: VariableDataRefState): DimSizes { const coordVariables = resource.coordVariables; if (!coordVariables) { return EMPTY_OBJECT; @@ -563,7 +579,8 @@ export function getWorldViewSelectedEntity(view: ViewState): Cesium.Entity return null; } -export function getWorldViewVectorLayerForEntity(view: ViewState, entity: Cesium.Entity | null): VectorLayerBase | null { +export function getWorldViewVectorLayerForEntity(view: ViewState, + entity: Cesium.Entity | null): VectorLayerBase | null { const externalObject = getWorldViewExternalObject(view); if (!entity || !externalObject) { return null; diff --git a/src/renderer/state.ts b/src/renderer/state.ts index fa265eb..2e2591a 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -257,11 +257,15 @@ export type OperationKWArgs = { [name: string]: OperationArg }; */ export interface WorkspaceState { /** - * The workspace's base directory path. + * The workspace' identifier. + */ + id: number; + /** + * The workspace' base directory path. */ baseDir: string; /** - * The workspace's description. + * The workspace' description. */ description: string | null; /** diff --git a/src/renderer/webapi/apis/WorkspaceAPI.ts b/src/renderer/webapi/apis/WorkspaceAPI.ts index bae00b8..3736eb8 100644 --- a/src/renderer/webapi/apis/WorkspaceAPI.ts +++ b/src/renderer/webapi/apis/WorkspaceAPI.ts @@ -47,6 +47,7 @@ function responseToWorkspace(workspaceResponse: any): WorkspaceState { return null; } return { + id: workspaceResponse.id, baseDir: workspaceResponse.base_dir, description: workspaceResponse.description, isScratch: workspaceResponse.is_scratch, From 54b1a20c4c65eee6ed5f3f788f0cfb225915d1d9 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Sat, 25 Mar 2023 18:16:44 +0100 Subject: [PATCH 07/20] Started 4.0.0-dev.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 721bf9e..d797421 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cate-app", - "version": "4.0.0-dev.0", + "version": "4.0.0-dev.1", "private": true, "homepage": ".", "dependencies": { From 4b746f9330f9de679c72d7beb3d492056495fe31 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 29 Mar 2023 18:33:34 +0200 Subject: [PATCH 08/20] Added base 16 codec --- src/common/base16codec.spec.ts | 18 ++++++++++++++++++ src/common/base16codec.ts | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 src/common/base16codec.spec.ts create mode 100644 src/common/base16codec.ts diff --git a/src/common/base16codec.spec.ts b/src/common/base16codec.spec.ts new file mode 100644 index 0000000..be02bb1 --- /dev/null +++ b/src/common/base16codec.spec.ts @@ -0,0 +1,18 @@ +import { expect } from 'chai'; +import Base16 from './base16codec'; + +describe('codec', function() { + it('works for empty string', function() { + expect(Base16.encode("")).to.equal(""); + expect(Base16.decode("")).to.equal(""); + }); + + it('works for a path string', function() { + expect(Base16.encode("/workspaces/bibo")).to.equal( + "2f776f726b7370616365732f6269626f" + ); + expect(Base16.decode("2f776f726b7370616365732f6269626f")).to.equal( + "/workspaces/bibo" + ); + }); +}); \ No newline at end of file diff --git a/src/common/base16codec.ts b/src/common/base16codec.ts new file mode 100644 index 0000000..15acf6c --- /dev/null +++ b/src/common/base16codec.ts @@ -0,0 +1,19 @@ +class Base16 { + static encode(s: string): string { + let result = ''; + for (let i = 0; i < s.length; i++) { + result += s.codePointAt(i).toString(16); + } + return result; + } + + static decode(s: string): string { + let result = ''; + for (let i = 0; i < s.length; i += 2) { + result += String.fromCharCode(parseInt(s.substr(i, 2), 16)); + } + return result; + } +} + +export default Base16; \ No newline at end of file From 219c1fe3aac6f6931c757c10f7a3773816798e89 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 30 Mar 2023 10:13:21 +0200 Subject: [PATCH 09/20] reformat --- src/common/base16codec.spec.ts | 26 +++++++++++++------------- src/common/base16codec.ts | 24 ++++++++++++------------ 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/common/base16codec.spec.ts b/src/common/base16codec.spec.ts index be02bb1..f132278 100644 --- a/src/common/base16codec.spec.ts +++ b/src/common/base16codec.spec.ts @@ -1,18 +1,18 @@ import { expect } from 'chai'; import Base16 from './base16codec'; -describe('codec', function() { - it('works for empty string', function() { - expect(Base16.encode("")).to.equal(""); - expect(Base16.decode("")).to.equal(""); - }); +describe('codec', function () { + it('works for empty string', function () { + expect(Base16.encode("")).to.equal(""); + expect(Base16.decode("")).to.equal(""); + }); - it('works for a path string', function() { - expect(Base16.encode("/workspaces/bibo")).to.equal( - "2f776f726b7370616365732f6269626f" - ); - expect(Base16.decode("2f776f726b7370616365732f6269626f")).to.equal( - "/workspaces/bibo" - ); - }); + it('works for a path string', function () { + expect(Base16.encode("/workspaces/bibo")).to.equal( + "2f776f726b7370616365732f6269626f" + ); + expect(Base16.decode("2f776f726b7370616365732f6269626f")).to.equal( + "/workspaces/bibo" + ); + }); }); \ No newline at end of file diff --git a/src/common/base16codec.ts b/src/common/base16codec.ts index 15acf6c..761dce4 100644 --- a/src/common/base16codec.ts +++ b/src/common/base16codec.ts @@ -1,19 +1,19 @@ class Base16 { - static encode(s: string): string { - let result = ''; - for (let i = 0; i < s.length; i++) { - result += s.codePointAt(i).toString(16); + static encode(s: string): string { + let result = ''; + for (let i = 0; i < s.length; i++) { + result += s.codePointAt(i).toString(16); + } + return result; } - return result; - } - static decode(s: string): string { - let result = ''; - for (let i = 0; i < s.length; i += 2) { - result += String.fromCharCode(parseInt(s.substr(i, 2), 16)); + static decode(s: string): string { + let result = ''; + for (let i = 0; i < s.length; i += 2) { + result += String.fromCharCode(parseInt(s.substr(i, 2), 16)); + } + return result; } - return result; - } } export default Base16; \ No newline at end of file From ae781f9c17a9c2d19d79515df33a7a2b844d80ba Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 30 Mar 2023 12:21:21 +0200 Subject: [PATCH 10/20] compute workspaceId from baseDir --- src/common/base16codec.spec.ts | 4 +-- src/common/base16codec.ts | 32 +++++++++++++++----- src/renderer/actions.ts | 2 +- src/renderer/containers/FigureView.tsx | 2 +- src/renderer/containers/GlobeView.tsx | 4 +-- src/renderer/containers/globe-view-layers.ts | 6 ++-- src/renderer/selectors.ts | 12 +++++--- src/renderer/state-util.ts | 21 ++++++++----- src/renderer/state.ts | 4 --- src/renderer/webapi/apis/WorkspaceAPI.ts | 1 - 10 files changed, 55 insertions(+), 33 deletions(-) diff --git a/src/common/base16codec.spec.ts b/src/common/base16codec.spec.ts index f132278..287be99 100644 --- a/src/common/base16codec.spec.ts +++ b/src/common/base16codec.spec.ts @@ -9,9 +9,9 @@ describe('codec', function () { it('works for a path string', function () { expect(Base16.encode("/workspaces/bibo")).to.equal( - "2f776f726b7370616365732f6269626f" + "2F776F726B7370616365732F6269626F" ); - expect(Base16.decode("2f776f726b7370616365732f6269626f")).to.equal( + expect(Base16.decode("2F776F726B7370616365732F6269626F")).to.equal( "/workspaces/bibo" ); }); diff --git a/src/common/base16codec.ts b/src/common/base16codec.ts index 761dce4..52cfb4a 100644 --- a/src/common/base16codec.ts +++ b/src/common/base16codec.ts @@ -1,16 +1,34 @@ +/** + * Encodes UTF-8 strings into lowercase hexadecimal representation and + * decodes hexadecimal representation into UTF-8. + */ class Base16 { - static encode(s: string): string { + static encode(value: string): string { let result = ''; - for (let i = 0; i < s.length; i++) { - result += s.codePointAt(i).toString(16); + for (let i = 0; i < value.length; i++) { + let part = value.codePointAt(i).toString(16); + if (part.length === 1) { + result += '0' + part; + } else if (part.length === 2) { + result += part; + } else { + throw new Error('string value must comprise utf-8 values only'); + } } - return result; + return result.toUpperCase(); } - static decode(s: string): string { + static decode(value: string): string { + if (value.length % 2 !== 0) { + throw new Error('string value length must be a multiple of two'); + } let result = ''; - for (let i = 0; i < s.length; i += 2) { - result += String.fromCharCode(parseInt(s.substr(i, 2), 16)); + for (let i = 0; i < value.length; i += 2) { + let code = parseInt(value.substr(i, 2), 16); + if (Number.isNaN(code)) { + throw new Error('string value must comprise hexadecimal values only'); + } + result += String.fromCharCode(code); } return result; } diff --git a/src/renderer/actions.ts b/src/renderer/actions.ts index cc044a6..a5aae8a 100644 --- a/src/renderer/actions.ts +++ b/src/renderer/actions.ts @@ -1950,7 +1950,7 @@ export function notifySelectedEntityChange(viewId: string, layer: LayerState | n const resId = selectedEntity['_resId']; const featureIndex = +selectedEntity['_idx']; const baseUrl = selectors.webAPIRestUrlSelector(getState()); - const workspaceId = workspace.id; + const workspaceId = selectors.workspaceIdSelector(getState()); const featureUrl = getFeatureUrl(baseUrl, workspaceId, {resId}, featureIndex); reloadEntityWithOriginalGeometry(selectedEntity, featureUrl, (layer as any).style); } diff --git a/src/renderer/containers/FigureView.tsx b/src/renderer/containers/FigureView.tsx index c6d63c9..faf2e39 100644 --- a/src/renderer/containers/FigureView.tsx +++ b/src/renderer/containers/FigureView.tsx @@ -15,7 +15,7 @@ interface IFigureViewOwnProps { interface IFigureViewProps extends IFigureViewOwnProps { baseUrl: string; - workspaceId: number | null; + workspaceId: string | null; figureResources: ResourceState[]; mplWebSocketUrl: string; } diff --git a/src/renderer/containers/GlobeView.tsx b/src/renderer/containers/GlobeView.tsx index 7001e74..eb133bc 100644 --- a/src/renderer/containers/GlobeView.tsx +++ b/src/renderer/containers/GlobeView.tsx @@ -24,7 +24,7 @@ import { ImageLayerDescriptor, LayerDescriptors } from '../components/cesium/CesiumGlobe'; -import { findVariableIndexCoordinates, PLACEMARK_ID_PREFIX } from '../state-util'; +import { findVariableIndexCoordinates, getWorkspaceId, PLACEMARK_ID_PREFIX } from '../state-util'; import { ViewState } from '../components/ViewState'; import { convertLayersToLayerDescriptors } from './globe-view-layers'; import * as Cesium from 'cesium'; @@ -268,7 +268,7 @@ class GlobeView extends React.Component { export const isScratchWorkspaceSelector = (state: State): boolean => { return state.data.workspace && state.data.workspace.isScratch; }; -export const workspaceIdSelector = (state: State): number | null => { - return state.data.workspace && state.data.workspace.id; -}; export const workspaceBaseDirSelector = (state: State): string | null => { return state.data.workspace && state.data.workspace.baseDir; }; @@ -546,6 +543,13 @@ export const selectedVariableNameSelector = (state: State): string | null => { return state.control.selectedVariableName; }; +export const workspaceIdSelector = createSelector( + workspaceSelector, + (workspace: WorkspaceState | null) => { + return workspace !== null ? getWorkspaceId(workspace) : null; + } +); + export const workspaceNameSelector = createSelector( workspaceBaseDirSelector, (baseDir: string | null) => { diff --git a/src/renderer/state-util.ts b/src/renderer/state-util.ts index a716945..b8b14ac 100644 --- a/src/renderer/state-util.ts +++ b/src/renderer/state-util.ts @@ -1,5 +1,6 @@ import * as Cesium from 'cesium'; import { IconName } from '@blueprintjs/core'; +import Base16 from '../common/base16codec'; import { AnimationViewDataState, @@ -25,7 +26,7 @@ import { VariableRefState, VariableState, VectorLayerBase, - WorldViewDataState + WorldViewDataState, WorkspaceState } from './state'; import { ViewState } from './components/ViewState'; import * as assert from '../common/assert'; @@ -79,6 +80,10 @@ export const MY_PLACES_LAYER = { export const PLACEMARK_ID_PREFIX = 'placemark-'; +export function getWorkspaceId(workspace: WorkspaceState): string { + return Base16.encode(workspace.baseDir); +} + export function isLocalDataStore(dataStore: DataStoreState | null) { return dataStore && (dataStore.id === 'local' || dataStore.isLocal); } @@ -196,7 +201,7 @@ function _checkDataSourceCapability(dataSource: DataSourceState, } export function getTileUrl(baseUrl: string, - workspaceId: number, + workspaceId: string, layer: VariableImageLayerState): string { return baseUrl + `ws/res/tile/${workspaceId}/${layer.resId}/{z}/{y}/{x}.png?` + `&var=${encodeURIComponent(layer.varName)}` @@ -207,20 +212,20 @@ export function getTileUrl(baseUrl: string, } export function getFeatureCollectionUrl(baseUrl: string, - workspaceId: number, + workspaceId: string, ref: ResourceRefState): string { return baseUrl + `ws/res/geojson/${workspaceId}/${ref.resId}`; } export function getFeatureUrl(baseUrl: string, - workspaceId: number, + workspaceId: string, ref: ResourceRefState, index: number): string { return baseUrl + `ws/res/geojson/${workspaceId}/${ref.resId}/${index}`; } export function getCsvUrl(baseUrl: string, - workspaceId: number, + workspaceId: string, ref: ResourceRefState, varName?: string | null): string { let varPart = ''; if (varName) { @@ -229,7 +234,7 @@ export function getCsvUrl(baseUrl: string, return baseUrl + `ws/res/csv/${workspaceId}/${ref.resId}${varPart}`; } -export function getHtmlUrl(baseUrl: string, workspaceId: number, resId: number): string { +export function getHtmlUrl(baseUrl: string, workspaceId: string, resId: number): string { return baseUrl + `ws/res/html/${workspaceId}/${resId}`; } @@ -237,11 +242,11 @@ export function getGeoJSONCountriesUrl(baseUrl: string): string { return baseUrl + 'ws/countries'; } -export function getMPLWebSocketUrl(baseUrl: string, workspaceId: number, figureId: number): string { +export function getMPLWebSocketUrl(baseUrl: string, workspaceId: string, figureId: number): string { return `${baseUrl}${workspaceId}/${figureId}`; } -export function getMPLDownloadUrl(baseUrl: string, workspaceId: number, figureId: number): string { +export function getMPLDownloadUrl(baseUrl: string, workspaceId: string, figureId: number): string { return `${baseUrl}mpl/download/${workspaceId}/${figureId}`; } diff --git a/src/renderer/state.ts b/src/renderer/state.ts index 2e2591a..27cf200 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -256,10 +256,6 @@ export type OperationKWArgs = { [name: string]: OperationArg }; * IMPORTANT: WorkspaceState must reflect what is returned by cate.core.workspace.Workspace.to_json_dict() */ export interface WorkspaceState { - /** - * The workspace' identifier. - */ - id: number; /** * The workspace' base directory path. */ diff --git a/src/renderer/webapi/apis/WorkspaceAPI.ts b/src/renderer/webapi/apis/WorkspaceAPI.ts index 3736eb8..bae00b8 100644 --- a/src/renderer/webapi/apis/WorkspaceAPI.ts +++ b/src/renderer/webapi/apis/WorkspaceAPI.ts @@ -47,7 +47,6 @@ function responseToWorkspace(workspaceResponse: any): WorkspaceState { return null; } return { - id: workspaceResponse.id, baseDir: workspaceResponse.base_dir, description: workspaceResponse.description, isScratch: workspaceResponse.is_scratch, From 0e1c85295476a75a204f017a1c88d1311349f2bd Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 30 Mar 2023 13:09:28 +0200 Subject: [PATCH 11/20] 4.0.0-dev.2 --- package.json | 2 +- src/version.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index d797421..18721d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cate-app", - "version": "4.0.0-dev.1", + "version": "4.0.0-dev.2", "private": true, "homepage": ".", "dependencies": { diff --git a/src/version.ts b/src/version.ts index e659fbe..81bda5b 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,5 +1,5 @@ // IMPORTANT: Any changes of CATE_APP_VERSION must be synchronized // with the version field in "../package.json". // -export const CATE_APP_VERSION = "4.0.0-dev.0"; +export const CATE_APP_VERSION = "4.0.0-dev.2"; From 3ad282dff5c68875a8808dbcbe9cb1e3bb9db87b Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Thu, 30 Mar 2023 19:03:55 +0200 Subject: [PATCH 12/20] Added two new functions: "open in browser" and "shutdown server" --- package.json | 2 +- src/renderer/actions.ts | 11 +++ src/renderer/containers/AppBar.tsx | 18 ++++ src/renderer/containers/AppMainPage.tsx | 8 +- ...alog.tsx => ServiceAutoShutdownDialog.tsx} | 8 +- .../containers/ServiceShutdownDialog.tsx | 83 +++++++++++++++++++ .../webapi/apis/ServiceShutdownAPI.ts | 10 +++ src/renderer/webapi/apis/index.ts | 1 + src/version.ts | 2 +- 9 files changed, 134 insertions(+), 9 deletions(-) rename src/renderer/containers/{ServiceAutoCloseDialog.tsx => ServiceAutoShutdownDialog.tsx} (92%) create mode 100644 src/renderer/containers/ServiceShutdownDialog.tsx create mode 100644 src/renderer/webapi/apis/ServiceShutdownAPI.ts diff --git a/package.json b/package.json index 18721d4..4afc3c2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cate-app", - "version": "4.0.0-dev.2", + "version": "4.0.0-dev.3", "private": true, "homepage": ".", "dependencies": { diff --git a/src/renderer/actions.ts b/src/renderer/actions.ts index a5aae8a..6461952 100644 --- a/src/renderer/actions.ts +++ b/src/renderer/actions.ts @@ -2175,6 +2175,17 @@ export function hidePreferencesDialog() { return hideDialog('preferencesDialog'); } +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// (User) Preferences actions + +export function showShutdownDialog() { + return showDialog('shutdownDialog'); +} + +export function hideShutdownDialog() { + return hideDialog('shutdownDialog'); +} + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Desktop-PWA installation support actions diff --git a/src/renderer/containers/AppBar.tsx b/src/renderer/containers/AppBar.tsx index 4f23746..e19f410 100644 --- a/src/renderer/containers/AppBar.tsx +++ b/src/renderer/containers/AppBar.tsx @@ -39,6 +39,14 @@ const _AppBar: React.FC = ( } ) => { + const handleOpenInBrowser = () => { + window.open(window.location.href, "_blank"); + }; + + const handleShutdownServer = () => { + dispatch(actions.showShutdownDialog()); + }; + const handlePreferencesClick = () => { dispatch(actions.showPreferencesDialog()); }; @@ -60,6 +68,16 @@ const _AppBar: React.FC = (
+ - - } position={PopoverPosition.BOTTOM}> - - + {/*} position={PopoverPosition.BOTTOM}>*/} + {/* */} + {/**/} } position={PopoverPosition.BOTTOM}> diff --git a/src/renderer/containers/AppMainPage.tsx b/src/renderer/containers/AppMainPage.tsx index 430848e..e084095 100644 --- a/src/renderer/containers/AppMainPage.tsx +++ b/src/renderer/containers/AppMainPage.tsx @@ -5,7 +5,7 @@ import { useMatomo } from '@datapunt/matomo-tracker-react' import GdprBanner from './GdprBanner'; import { isElectron } from '../electron'; -import { FileSystemAPI, ServiceShutdownAPI } from '../webapi'; +import { FileSystemAPI } from '../webapi'; import AppBar from './AppBar'; import ChooseWorkspaceDialog, { DELETE_WORKSPACE_DIALOG_ID, OPEN_WORKSPACE_DIALOG_ID } from './ChooseWorkspaceDialog'; import GlobeView from './GlobeView' diff --git a/src/renderer/containers/PreferencesDialog.tsx b/src/renderer/containers/PreferencesDialog.tsx index d073cdd..aa11adb 100644 --- a/src/renderer/containers/PreferencesDialog.tsx +++ b/src/renderer/containers/PreferencesDialog.tsx @@ -122,7 +122,6 @@ class PreferencesDialog extends React.Component - ); @@ -152,14 +151,6 @@ class PreferencesDialog extends React.Component - {this.renderProxyUrlInput()} - - ); - } - private renderSystemPanel() { const showProperties = this.getStateValue('showProperties', false); const onShowPropertiesChange = this.getChangeHandler('showProperties', false); @@ -220,9 +211,9 @@ class PreferencesDialog extends React.Component )} - File access mode: + Filesystem access: - {this.props.serviceInfo.userRootMode ? 'sandboxed file system' : 'full access to file system'} + {this.props.serviceInfo.userRootMode ? 'restricted' : 'unrestricted'} @@ -293,23 +284,6 @@ class PreferencesDialog extends React.Component - Proxy URL: - - - ); - } - ///////////////////////////////////////////////////////////////////////////////////////////////////////////// // Components // Note (forman): could make this React component later diff --git a/src/renderer/webapi/apis/ServiceShutdownAPI.ts b/src/renderer/webapi/apis/ServiceShutdownAPI.ts index 0ebb63a..c61ac47 100644 --- a/src/renderer/webapi/apis/ServiceShutdownAPI.ts +++ b/src/renderer/webapi/apis/ServiceShutdownAPI.ts @@ -1,10 +1,7 @@ export class ServiceShutdownAPI { shutdown(serviceURL: string): Promise { - const url = new URL(serviceURL + "/kill"); return fetch(serviceURL + "/exit") - .then((response: Response) => { - return null; - }); + .then(() => null); } } diff --git a/src/version.ts b/src/version.ts index 3fcc306..5c2a50f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,5 +1,5 @@ // IMPORTANT: Any changes of CATE_APP_VERSION must be synchronized // with the version field in "../package.json". // -export const CATE_APP_VERSION = "4.0.0-dev.3"; +export const CATE_APP_VERSION = "4.0.0-dev.5"; From 95ee8e3a87e9142efd61f1b3fd48fc63e5cc2a49 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Tue, 4 Apr 2023 08:03:03 +0200 Subject: [PATCH 18/20] Apply suggestions from code review Co-authored-by: Tonio Fincke --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 94e7379..c9093b5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,7 @@ ### Changes 4.0.0 (in development) * Since Cate App is now designed to work inside of Jupyter Lab and standalone, - the user login action is no longer required because users already log into + The user login action is no longer required because users already log into JupyterLab. Therefore, all code dealing with user login and Cate service provisioning in the cloud has been removed. Other changes include: - Added a new user action to open Cate App in a browser tab available, if From fdf91222ec910cbc41a9e5a7322fac909c2f74e2 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 12 Apr 2023 17:00:17 +0200 Subject: [PATCH 19/20] Slightly improved readability of dataset titles --- CHANGES.md | 3 +++ package.json | 2 +- src/renderer/components/DataSourceItem.tsx | 29 ++++++++++++++++------ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c9093b5..c88d9a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,9 @@ [jupyter-server-proxy](https://jupyter-server-proxy.readthedocs.io/). +* Slightly improved readability of dataset titles generated from identifiers + if titles are missing. + ### Changes 3.1.4 * Updated to new keycloak version diff --git a/package.json b/package.json index 3856aa8..a281b7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cate-app", - "version": "4.0.0-dev.5", + "version": "4.0.0-dev.6", "private": true, "homepage": ".", "dependencies": { diff --git a/src/renderer/components/DataSourceItem.tsx b/src/renderer/components/DataSourceItem.tsx index 82a9b8b..ddfcd35 100644 --- a/src/renderer/components/DataSourceItem.tsx +++ b/src/renderer/components/DataSourceItem.tsx @@ -64,7 +64,7 @@ const DataSourceItem: React.FC = ({dataSource, showDataSour
{dataSource.id}
) : ( - {warnIcon}{title}{typeSpec} +
{warnIcon}{title}{typeSpec}
)} ); @@ -73,16 +73,29 @@ const DataSourceItem: React.FC = ({dataSource, showDataSour export default DataSourceItem; +const SKIPPED_PREFIXES: string[] = ['esacci ']; +const SKIPPED_SUFFIXES: string[] = ['.zarr', '.levels', '.nc', '.tif']; + function dataSourceToTitle(dataSource: DataSourceState) { - const title = dataSource.id - .replace(/\./g, ' ') + let title = dataSource.id .replace(/-/g, ' ') - .replace(/_/g, '-'); - if (title.startsWith('esacci ')) { - return title.substr('esacci '.length); - } else if (title.startsWith('ESACCI ')) { - return title.substr('ESACCI '.length); + .replace(/_/g, ' '); + const lastSlashPos = title.lastIndexOf('/'); + if (lastSlashPos >= 0 && lastSlashPos < title.length - 1) { + title = title.substring(lastSlashPos + 1) + } + for (let prefix of SKIPPED_PREFIXES) { + if (title.startsWith(prefix) || title.startsWith(prefix.toUpperCase())) { + title = title.substring(prefix.length); + break; + } + } + for (let suffix of SKIPPED_SUFFIXES) { + if (title.endsWith(suffix) || title.endsWith(suffix.toUpperCase())) { + title = title.substring(0, title.length - suffix.length); + break; + } } return title; } From c4e865a02e97a8df2bb8988505bd1dfae8458052 Mon Sep 17 00:00:00 2001 From: Norman Fomferra Date: Wed, 12 Apr 2023 17:00:17 +0200 Subject: [PATCH 20/20] Slightly improved readability of dataset titles --- CHANGES.md | 3 +++ package.json | 2 +- src/renderer/components/DataSourceItem.tsx | 29 ++++++++++++++++------ 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c9093b5..c88d9a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -30,6 +30,9 @@ [jupyter-server-proxy](https://jupyter-server-proxy.readthedocs.io/). +* Slightly improved readability of dataset titles generated from identifiers + if titles are missing. + ### Changes 3.1.4 * Updated to new keycloak version diff --git a/package.json b/package.json index 3856aa8..a281b7b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cate-app", - "version": "4.0.0-dev.5", + "version": "4.0.0-dev.6", "private": true, "homepage": ".", "dependencies": { diff --git a/src/renderer/components/DataSourceItem.tsx b/src/renderer/components/DataSourceItem.tsx index 82a9b8b..ddfcd35 100644 --- a/src/renderer/components/DataSourceItem.tsx +++ b/src/renderer/components/DataSourceItem.tsx @@ -64,7 +64,7 @@ const DataSourceItem: React.FC = ({dataSource, showDataSour
{dataSource.id}
) : ( - {warnIcon}{title}{typeSpec} +
{warnIcon}{title}{typeSpec}
)} ); @@ -73,16 +73,29 @@ const DataSourceItem: React.FC = ({dataSource, showDataSour export default DataSourceItem; +const SKIPPED_PREFIXES: string[] = ['esacci ']; +const SKIPPED_SUFFIXES: string[] = ['.zarr', '.levels', '.nc', '.tif']; + function dataSourceToTitle(dataSource: DataSourceState) { - const title = dataSource.id - .replace(/\./g, ' ') + let title = dataSource.id .replace(/-/g, ' ') - .replace(/_/g, '-'); - if (title.startsWith('esacci ')) { - return title.substr('esacci '.length); - } else if (title.startsWith('ESACCI ')) { - return title.substr('ESACCI '.length); + .replace(/_/g, ' '); + const lastSlashPos = title.lastIndexOf('/'); + if (lastSlashPos >= 0 && lastSlashPos < title.length - 1) { + title = title.substring(lastSlashPos + 1) + } + for (let prefix of SKIPPED_PREFIXES) { + if (title.startsWith(prefix) || title.startsWith(prefix.toUpperCase())) { + title = title.substring(prefix.length); + break; + } + } + for (let suffix of SKIPPED_SUFFIXES) { + if (title.endsWith(suffix) || title.endsWith(suffix.toUpperCase())) { + title = title.substring(0, title.length - suffix.length); + break; + } } return title; }