diff --git a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls index 04f0bc0b15..7c39847187 100644 --- a/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls +++ b/server/bundles/io.cloudbeaver.server/schema/service.core.graphqls @@ -92,6 +92,8 @@ type ServerConfig { serverURL: String! + rootURI: String! + anonymousAccessEnabled: Boolean authenticationEnabled: Boolean @deprecated supportsCustomConnections: Boolean diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java index 611b731d85..cb19130906 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/model/WebServerConfig.java @@ -64,6 +64,11 @@ public class WebServerConfig { return CommonUtils.notEmpty(application.getServerURL()); } + @Property + public String getRootURI() { + return CommonUtils.notEmpty(application.getRootURI()); + } + @Property public boolean isAnonymousAccessEnabled() { return application.getAppConfiguration().isAnonymousAccessEnabled(); diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java index 4d212c4d52..b85a421854 100644 --- a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CBStaticServlet.java @@ -1,5 +1,7 @@ package io.cloudbeaver.server.jetty; +import io.cloudbeaver.server.CBApplication; +import org.eclipse.jetty.http.HttpContent; import org.eclipse.jetty.http.HttpField; import org.eclipse.jetty.http.HttpHeader; import org.eclipse.jetty.server.ResourceService; @@ -9,7 +11,10 @@ import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Enumeration; @WebServlet(urlPatterns = "/") public class CBStaticServlet extends DefaultServlet { @@ -26,10 +31,33 @@ public class CBStaticServlet extends DefaultServlet { } private static ResourceService makeResourceService() { - ResourceService resourceService = new ResourceService(); + ResourceService resourceService = new ProxyResourceService(); resourceService.setCacheControl(new HttpField(HttpHeader.CACHE_CONTROL, "public, max-age=" + STATIC_CACHE_SECONDS)); return resourceService; } + private static class ProxyResourceService extends ResourceService { + @Override + protected boolean sendData(HttpServletRequest request, HttpServletResponse response, boolean include, HttpContent content, Enumeration reqRanges) throws IOException { + if (content.getResource().getName().endsWith("index.html")) { + return patchIndexHtml(response, content); + } + return super.sendData(request, response, include, content, reqRanges); + } + + private boolean patchIndexHtml(HttpServletResponse response, HttpContent content) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + content.getResource().writeTo(baos, 0, content.getContentLengthValue()); + String indexContents = new String(baos.toByteArray(), StandardCharsets.UTF_8); + indexContents = indexContents.replace("{ROOT_URI}", CBApplication.getInstance().getRootURI()); + byte[] indexBytes = indexContents.getBytes(StandardCharsets.UTF_8); + + putHeaders(response, content, indexBytes.length); + response.getOutputStream().write(indexBytes); + + return true; + } + } + } \ No newline at end of file diff --git a/webapp/package.json b/webapp/package.json index 77490e938f..8a02b369e3 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -39,6 +39,7 @@ "mobx": "^6.3.0", "mobx-react-lite": "^3.2.0", "node-sass": "^5.0.0", + "path-browserify": "^1.0.1", "preact": "^10.5.13", "preact-jsx-runtime": "^1.2.0", "preact-render-to-string": "^5.1.19", diff --git a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts index f08238f4eb..43dcb7eda9 100644 --- a/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts +++ b/webapp/packages/core-administration/src/AdministrationScreen/AdministrationScreenService.ts @@ -13,7 +13,7 @@ import { IExecutor, Executor } from '@cloudbeaver/core-executor'; import { PermissionsResource, PermissionsService, ServerConfigResource } from '@cloudbeaver/core-root'; import { ScreenService, RouterState } from '@cloudbeaver/core-routing'; import { LocalStorageSaveService } from '@cloudbeaver/core-settings'; -import { BuildVersion } from '@cloudbeaver/core-utils'; +import { GlobalConstants } from '@cloudbeaver/core-utils'; import { AdministrationItemService } from '../AdministrationItem/AdministrationItemService'; import type { IAdministrationItemRoute } from '../AdministrationItem/IAdministrationItemRoute'; @@ -76,7 +76,7 @@ export class AdministrationScreenService { this.info = { workspaceId: '', - version: BuildVersion.version || '', + version: GlobalConstants.version || '', serverVersion: '', configurationMode: false, }; @@ -154,13 +154,13 @@ export class AdministrationScreenService { this.info.workspaceId !== this.serverConfigResource.workspaceId || this.info.configurationMode !== this.isConfigurationMode || this.info.serverVersion !== this.serverConfigResource.serverVersion - || this.info.version !== BuildVersion.version + || this.info.version !== GlobalConstants.version ) { this.clearItemsState(); this.info.workspaceId = this.serverConfigResource.workspaceId; this.info.configurationMode = this.isConfigurationMode; this.info.serverVersion = this.serverConfigResource.serverVersion; - this.info.version = BuildVersion.version || ''; + this.info.version = GlobalConstants.version || ''; } if (defaultState) { diff --git a/webapp/packages/core-app/public/icons/icons.svg b/webapp/packages/core-app/public/icons/icons.svg index 3c85ed29fc..bcefbfc39d 100644 --- a/webapp/packages/core-app/public/icons/icons.svg +++ b/webapp/packages/core-app/public/icons/icons.svg @@ -186,18 +186,10 @@ - - - - - - - - - + + - - - + + diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts index 3674ce5d3e..0acfd9ab2e 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts @@ -79,7 +79,7 @@ export class NavigationTreeService { return false; } } - await this.navNodeManagerService.loadTree(id); + await this.navTreeResource.load(id); return true; } catch (exception) { this.notificationService.logException(exception); diff --git a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts index 89f1c344da..fe7cd5b01c 100644 --- a/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts +++ b/webapp/packages/core-app/src/NavigationTree/useElementsTree.ts @@ -240,13 +240,13 @@ export function useElementsTree(options: IOptions): IElementsTree { useExecutor({ executor: navNodeInfoResource.onDataOutdated, - handlers: [function refreshRoot() { + postHandlers: [function refreshRoot() { loadTree(options.root); }], }); useExecutor({ executor: navTreeResource.onNodeRefresh, - handlers: [loadTree], + postHandlers: [loadTree], }); useExecutor({ diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index 7b89fbfdc6..b148304968 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -226,7 +226,7 @@ export class NavTreeResource extends CachedMapResource { private async connectionUpdateHandler(key: ResourceKey) { await this.markOutdated(ROOT_NODE_PATH); - ResourceKeyUtils.forEach(key, async key => { + await ResourceKeyUtils.forEachAsync(key, async key => { const nodeId = NodeManagerUtils.connectionIdToConnectionNodeId(key); if (this.has(nodeId)) { @@ -235,14 +235,14 @@ export class NavTreeResource extends CachedMapResource { if (!connectionInfo?.connected) { this.delete(nodeId); } else { - this.markTreeOutdated(nodeId); + await this.markTreeOutdated(nodeId); } } const node = this.navNodeInfoResource.get(nodeId); if (node) { - this.markOutdated(node.parentId); + await this.markOutdated(node.parentId); } }); } diff --git a/webapp/packages/core-app/src/useAppVersion.tsx b/webapp/packages/core-app/src/useAppVersion.tsx index aa96bdce83..9edec9a5d8 100644 --- a/webapp/packages/core-app/src/useAppVersion.tsx +++ b/webapp/packages/core-app/src/useAppVersion.tsx @@ -8,7 +8,7 @@ import { useService } from '@cloudbeaver/core-di'; import { ServerService } from '@cloudbeaver/core-root'; -import { BuildVersion } from '@cloudbeaver/core-utils'; +import { GlobalConstants } from '@cloudbeaver/core-utils'; interface IAppVersion { backendVersion: string; @@ -20,7 +20,7 @@ const VERSION_REGEX = /(\d+\.\d+\.\d+)/; export function useAppVersion(short = false): IAppVersion { const serverService = useService(ServerService); let backendVersion = serverService.config.data?.version || ''; - let frontendVersion = BuildVersion.version || ''; + let frontendVersion = GlobalConstants.version || ''; if (short) { backendVersion = VERSION_REGEX.exec(backendVersion)?.[1] ?? backendVersion; diff --git a/webapp/packages/core-authentication/src/UserInfoResource.ts b/webapp/packages/core-authentication/src/UserInfoResource.ts index 50f5d3d9f5..69afe4132d 100644 --- a/webapp/packages/core-authentication/src/UserInfoResource.ts +++ b/webapp/packages/core-authentication/src/UserInfoResource.ts @@ -59,16 +59,15 @@ export class UserInfoResource extends CachedDataResource } async login(provider: string, credentials: Record, link?: boolean): Promise { - const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); - - const { authToken } = await this.graphQLService.sdk.authLogin({ - provider, - credentials: processedCredentials, - linkUser: link, - customIncludeOriginDetails: true, - }); - await this.performUpdate(undefined, undefined, async () => { + const processedCredentials = await this.authProviderService.processCredentials(provider, credentials); + + const { authToken } = await this.graphQLService.sdk.authLogin({ + provider, + credentials: processedCredentials, + linkUser: link, + customIncludeOriginDetails: true, + }); if (this.data === null || link) { this.data = await this.loader(); } else { diff --git a/webapp/packages/core-blocks/src/Button.tsx b/webapp/packages/core-blocks/src/Button.tsx index c615552084..c77f3de873 100644 --- a/webapp/packages/core-blocks/src/Button.tsx +++ b/webapp/packages/core-blocks/src/Button.tsx @@ -58,6 +58,10 @@ const buttonStyles = composes( opacity: 0; } } + + &[href] { + text-decoration: none; + } } ` diff --git a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx index 0687244421..0c6050096c 100644 --- a/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/Checkboxes/FieldCheckboxNew.tsx @@ -30,6 +30,9 @@ const fieldCheckboxStyles = css` padding-left: 10px; line-height: 16px; } + Checkbox[disabled] + field-label { + cursor: auto; + } `; export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({ @@ -44,9 +47,15 @@ export const FieldCheckboxNew: CheckboxType = function FieldCheckboxNew({ } return styled(styles)( - + - {children} + + {children} + ); }; diff --git a/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx index ad78555776..380abc3f71 100644 --- a/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx +++ b/webapp/packages/core-blocks/src/FormControls/InputFieldNew.tsx @@ -7,26 +7,54 @@ */ import { observer } from 'mobx-react-lite'; -import { useCallback, useContext } from 'react'; +import { useCallback, useContext, useState } from 'react'; import styled, { use, css } from 'reshadow'; -import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; +import { useTranslate } from '@cloudbeaver/core-localization'; +import { ComponentStyle, composes, useStyles } from '@cloudbeaver/core-theming'; import type { ILayoutSizeProps } from '../Containers/ILayoutSizeProps'; +import { Icon } from '../Icon'; import { baseFormControlStylesNew } from './baseFormControlStylesNew'; import { FormContext } from './FormContext'; import { isControlPresented } from './isControlPresented'; -const INPUT_FIELD_STYLES = css` - field-label { - display: block; - composes: theme-typography--body1 from global; - font-weight: 500; - } - field-label:not(:empty) { - padding-bottom: 10px; - } -`; +const INPUT_FIELD_STYLES = composes( + css` + Icon { + composes: theme-text-on-secondary from global; + } +`, + css` + field-label { + display: block; + composes: theme-typography--body1 from global; + font-weight: 500; + } + field-label:not(:empty) { + padding-bottom: 10px; + } + input-container { + position: relative; + } + icon-container { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + display: flex; + width: 24px; + height: 24px; + cursor: pointer; + } + input[disabled] + Icon { + cursor: auto; + opacity: 0.8; + } + input:not(:only-child) { + padding-right: 32px !important; + } +`); type BaseProps = Omit, 'onChange' | 'name' | 'value'> & ILayoutSizeProps & { description?: string; @@ -80,9 +108,19 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ onChange, ...rest }: ControlledProps | ObjectProps, ref: React.Ref) { + const [passwordRevealed, setPasswordRevealed] = useState(false); + const translate = useTranslate(); const styles = useStyles(baseFormControlStylesNew, INPUT_FIELD_STYLES, style); const context = useContext(FormContext); + const revealPassword = useCallback(() => { + if (rest.disabled) { + return; + } + + setPasswordRevealed(prev => !prev); + }, [rest.disabled]); + const handleChange = useCallback((event: React.ChangeEvent) => { const value = mapValue?.(event.target.value) ?? event.target.value; @@ -111,21 +149,37 @@ export const InputFieldNew: InputFieldType = observer(function InputFieldNew({ value = mapState(value); } + const showRevealPasswordButton = rest.type === 'password' && !rest.readOnly; + return styled(styles)( - - {children}{required && ' *'} - + + {children}{required && ' *'} + + + {showRevealPasswordButton && ( + + + + )} + {description && ( - + {description} )} diff --git a/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs b/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs index d751d941ce..bf06afaa32 100644 --- a/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs +++ b/webapp/packages/core-blocks/src/HTMLTemplates/head.html.ejs @@ -6,4 +6,7 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/webapp/packages/core-blocks/src/Icon.tsx b/webapp/packages/core-blocks/src/Icon.tsx index 968297c137..29000eb187 100644 --- a/webapp/packages/core-blocks/src/Icon.tsx +++ b/webapp/packages/core-blocks/src/Icon.tsx @@ -6,12 +6,15 @@ * you may not use this file except in compliance with the License. */ +import path from 'path'; import type { SVGAttributes } from 'react'; +import { GlobalConstants } from '@cloudbeaver/core-utils'; + export const Icon: React.FC> = function Icon(props) { const { name = '', className, ...rest } = props; const cn = `icon ${className || ''}`; - const url = name.startsWith('/') ? name : `icons/icons.svg#${name}`; + const url = name.startsWith('/') ? path.join(GlobalConstants.rootURI, name) : `icons/icons.svg#${name}`; return (