From 0d8efc47656c4bd0ef6df345575e873997397802 Mon Sep 17 00:00:00 2001 From: Maciej Bodek <1871646+insmac@users.noreply.github.com> Date: Mon, 4 Nov 2024 14:22:22 +0100 Subject: [PATCH 1/4] feat(web-console): add news image zoom (#350) --- packages/browser-tests/questdb | 2 +- .../web-console/src/scenes/Layout/index.tsx | 3 + .../src/scenes/News/image-zoom.tsx | 87 +++++++++++++++++++ .../web-console/src/scenes/News/index.tsx | 41 ++++++++- .../web-console/src/scenes/News/thumbnail.tsx | 24 +++-- .../web-console/src/store/Console/actions.ts | 7 ++ .../web-console/src/store/Console/reducers.ts | 8 ++ .../src/store/Console/selectors.ts | 7 +- .../web-console/src/store/Console/types.ts | 15 ++++ packages/web-console/src/utils/questdb.ts | 25 +++--- 10 files changed, 199 insertions(+), 20 deletions(-) create mode 100644 packages/web-console/src/scenes/News/image-zoom.tsx diff --git a/packages/browser-tests/questdb b/packages/browser-tests/questdb index 3ae8efbe0..6442b320c 160000 --- a/packages/browser-tests/questdb +++ b/packages/browser-tests/questdb @@ -1 +1 @@ -Subproject commit 3ae8efbe0772c568859f372a2c120055278f3c7b +Subproject commit 6442b320c1dd2b17ad9ea0f688600e9d7caf9258 diff --git a/packages/web-console/src/scenes/Layout/index.tsx b/packages/web-console/src/scenes/Layout/index.tsx index 007eb2095..ba14d4f73 100644 --- a/packages/web-console/src/scenes/Layout/index.tsx +++ b/packages/web-console/src/scenes/Layout/index.tsx @@ -36,6 +36,7 @@ import { CreateTableDialog } from "../../components/CreateTableDialog" import { EditorProvider } from "../../providers" import { Help } from "./help" import { Warnings } from "./warning" +import { ImageZoom } from "../News/image-zoom" import "allotment/dist/style.css" @@ -62,6 +63,7 @@ const Root = styled.div` ` const Main = styled.div<{ sideOpened: boolean }>` + position: relative; flex: 1; display: flex; width: ${({ sideOpened }) => @@ -81,6 +83,7 @@ const Layout = () => {
+ diff --git a/packages/web-console/src/scenes/News/image-zoom.tsx b/packages/web-console/src/scenes/News/image-zoom.tsx new file mode 100644 index 000000000..00ff19e35 --- /dev/null +++ b/packages/web-console/src/scenes/News/image-zoom.tsx @@ -0,0 +1,87 @@ +import React, { useEffect, useRef, useState } from "react" +import styled from "styled-components" +import { Box } from "@questdb/react-components" +import { useSelector, useDispatch } from "react-redux" +import { selectors, actions } from "../../store" +import { Thumbnail } from "./thumbnail" + +const Root = styled(Box).attrs({ align: "center", justifyContent: "center" })<{ + visible: boolean +}>` + width: 100%; + height: 100%; + position: absolute; + z-index: 1000; + opacity: ${({ visible }) => (visible ? 1 : 0)}; + pointer-events: ${({ visible }) => (visible ? "auto" : "none")}; +` + +const Overlay = styled.div<{ visible: boolean }>` + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background: rgba(33, 34, 44, 0.9); + opacity: ${({ visible }) => (visible ? 1 : 0)}; + pointer-events: ${({ visible }) => (visible ? "auto" : "none")}; + transition: opacity 0.2s ease-in-out; +` + +const Wrapper = styled.div` + z-index: 1001; + + img { + border: 1px solid ${({ theme }) => theme.color.offWhite}; + border-radius: ${({ theme }) => theme.borderRadius}; + } +` + +export const ImageZoom = () => { + const imageToZoom = useSelector(selectors.console.getImageToZoom) + const dispatch = useDispatch() + const rootRef = useRef(null) + const [rootWidth, setRootWidth] = useState(0) + const [rootHeight, setRootHeight] = useState(0) + const activeSidebar = useSelector(selectors.console.getActiveSidebar) + + const handleEsc = (event: KeyboardEvent) => { + if (event.key === "Escape") { + dispatch(actions.console.setImageToZoom(undefined)) + } + } + + useEffect(() => { + if (rootRef.current) { + setRootWidth(rootRef.current.offsetWidth) + setRootHeight(rootRef.current.offsetHeight) + } + }, [imageToZoom]) + + useEffect(() => { + if (activeSidebar === "news") { + document.addEventListener("keydown", handleEsc) + } else { + document.removeEventListener("keydown", handleEsc) + } + }, [activeSidebar]) + + if (activeSidebar !== "news") { + return null + } + + return ( + + + {imageToZoom && ( + + + + )} + + ) +} diff --git a/packages/web-console/src/scenes/News/index.tsx b/packages/web-console/src/scenes/News/index.tsx index abe9133ac..08503dd72 100644 --- a/packages/web-console/src/scenes/News/index.tsx +++ b/packages/web-console/src/scenes/News/index.tsx @@ -60,13 +60,15 @@ const NewsText = styled(Text).attrs({ color: "foreground" })` font-size: 1.6rem; } - p { + p, + li { font-size: ${({ theme }) => theme.fontSize.lg}; line-height: 1.75; } code { background-color: ${({ theme }) => theme.color.selection}; + color: ${({ theme }) => theme.color.pink}; padding: 0.2rem 0.4rem; border-radius: 0.2rem; } @@ -92,6 +94,8 @@ const News = () => { const [hasUnreadNews, setHasUnreadNews] = useState(false) const activeSidebar = useSelector(selectors.console.getActiveSidebar) + let hoverTimeout: NodeJS.Timeout + const getEnterpriseNews = async () => { setIsLoading(true) setHasError(false) @@ -229,10 +233,45 @@ const News = () => { newsItem.thumbnail[0].thumbnails.large && ( { + if (newsItem.thumbnail) { + hoverTimeout = setTimeout(() => { + if (newsItem && newsItem.thumbnail) { + dispatch( + actions.console.setImageToZoom({ + src: newsItem.thumbnail[0].thumbnails + .large.url, + width: + newsItem.thumbnail[0].thumbnails.large + .width, + height: + newsItem.thumbnail[0].thumbnails.large + .height, + alt: newsItem.title, + }), + ) + } + }, 500) + } + }, + onMouseOut: () => { + clearTimeout(hoverTimeout) + setTimeout(() => { + dispatch( + actions.console.setImageToZoom(undefined), + ) + }, 250) + }, + } + : {})} /> )} diff --git a/packages/web-console/src/scenes/News/thumbnail.tsx b/packages/web-console/src/scenes/News/thumbnail.tsx index 2964b5226..71dbd9e41 100644 --- a/packages/web-console/src/scenes/News/thumbnail.tsx +++ b/packages/web-console/src/scenes/News/thumbnail.tsx @@ -18,13 +18,12 @@ const Root = styled.div` } ` -const ThumbImg = styled.img<{ loaded: boolean }>` - width: 46rem; +const ThumbImg = styled.img<{ loaded: boolean; fadeIn?: boolean }>` height: auto; - ${({ loaded }) => ` + ${({ loaded, fadeIn }) => ` opacity: ${loaded ? 1 : 0}; - transition: opacity 0.2s ease-in-out; + ${fadeIn && `transition: opacity 0.2s ease-in-out;`} `} ` export const Thumbnail = ({ @@ -33,17 +32,27 @@ export const Thumbnail = ({ width, height, containerWidth, + containerHeight, + fadeIn, + ...rest }: { src: string alt: string width: number height: number containerWidth: number + containerHeight: number + fadeIn?: boolean }) => { const [isLoaded, setIsLoaded] = useState(false) - const scaledImageWidth = containerWidth - const scaledImageHeight = (scaledImageWidth / width) * height + let scaledImageWidth = containerWidth + let scaledImageHeight = (scaledImageWidth / width) * height + if (scaledImageHeight > containerHeight) { + const ratio = containerHeight / scaledImageHeight + scaledImageHeight = containerHeight + scaledImageWidth *= ratio + } useEffect(() => { const imgElement = new Image() @@ -55,7 +64,7 @@ export const Thumbnail = ({ }, [src]) return ( - + {!isLoaded && } ) diff --git a/packages/web-console/src/store/Console/actions.ts b/packages/web-console/src/store/Console/actions.ts index 2a1712147..3961d49f8 100644 --- a/packages/web-console/src/store/Console/actions.ts +++ b/packages/web-console/src/store/Console/actions.ts @@ -24,6 +24,7 @@ import { ConsoleAction, ConsoleAT, + ImageToZoom, TopPanel, Sidebar, BottomPanel, @@ -43,6 +44,11 @@ const setActiveBottomPanel = (panel: BottomPanel): ConsoleAction => ({ type: ConsoleAT.SET_ACTIVE_BOTTOM_PANEL, }) +const setImageToZoom = (image?: ImageToZoom): ConsoleAction => ({ + payload: image, + type: ConsoleAT.SET_IMAGE_TO_ZOOM, +}) + const toggleSideMenu = (): ConsoleAction => ({ type: ConsoleAT.TOGGLE_SIDE_MENU, }) @@ -52,4 +58,5 @@ export default { setActiveTopPanel, setActiveSidebar, setActiveBottomPanel, + setImageToZoom, } diff --git a/packages/web-console/src/store/Console/reducers.ts b/packages/web-console/src/store/Console/reducers.ts index 480d021fd..904d8757f 100644 --- a/packages/web-console/src/store/Console/reducers.ts +++ b/packages/web-console/src/store/Console/reducers.ts @@ -29,6 +29,7 @@ export const initialState: ConsoleStateShape = { activeTopPanel: "tables", activeSidebar: undefined, activeBottomPanel: "zeroState", + imageToZoom: undefined, } const _console = ( @@ -64,6 +65,13 @@ const _console = ( } } + case ConsoleAT.SET_IMAGE_TO_ZOOM: { + return { + ...state, + imageToZoom: action.payload, + } + } + default: return state } diff --git a/packages/web-console/src/store/Console/selectors.ts b/packages/web-console/src/store/Console/selectors.ts index f45be1e29..5c3899a71 100644 --- a/packages/web-console/src/store/Console/selectors.ts +++ b/packages/web-console/src/store/Console/selectors.ts @@ -21,7 +21,7 @@ * limitations under the License. * ******************************************************************************/ -import { StoreShape, Sidebar, BottomPanel, TopPanel } from "types" +import { StoreShape, Sidebar, BottomPanel, TopPanel, ImageToZoom } from "types" const getSideMenuOpened: (store: StoreShape) => boolean = (store) => store.console.sideMenuOpened @@ -35,9 +35,14 @@ const getActiveSidebar: (store: StoreShape) => Sidebar = (store) => const getActiveBottomPanel: (store: StoreShape) => BottomPanel = (store) => store.console.activeBottomPanel +const getImageToZoom: (store: StoreShape) => ImageToZoom | undefined = ( + store, +) => store.console.imageToZoom + export default { getSideMenuOpened, getActiveTopPanel, getActiveSidebar, getActiveBottomPanel, + getImageToZoom, } diff --git a/packages/web-console/src/store/Console/types.ts b/packages/web-console/src/store/Console/types.ts index 2b0f73891..76a456b0f 100644 --- a/packages/web-console/src/store/Console/types.ts +++ b/packages/web-console/src/store/Console/types.ts @@ -28,11 +28,19 @@ export type Sidebar = "news" | "create" | undefined export type BottomPanel = "result" | "zeroState" | "import" +export type ImageToZoom = { + src: string + alt: string + width: number + height: number +} + export type ConsoleStateShape = Readonly<{ sideMenuOpened: boolean activeTopPanel: TopPanel activeSidebar: Sidebar activeBottomPanel: BottomPanel + imageToZoom: ImageToZoom | undefined }> export enum ConsoleAT { @@ -40,6 +48,7 @@ export enum ConsoleAT { SET_ACTIVE_TOP_PANEL = "CONSOLE/SET_ACTIVE_TOP_PANEL", SET_ACTIVE_SIDEBAR = "CONSOLE/SET_ACTIVE_SIDEBAR", SET_ACTIVE_BOTTOM_PANEL = "CONSOLE/SET_ACTIVE_BOTTOM_PANEL", + SET_IMAGE_TO_ZOOM = "CONSOLE/SET_IMAGE_TO_ZOOM", } type ToggleSideMenuAction = Readonly<{ @@ -61,8 +70,14 @@ type setActiveBottomPanelAction = Readonly<{ type: ConsoleAT.SET_ACTIVE_BOTTOM_PANEL }> +type setImageToZoomAction = Readonly<{ + payload?: ImageToZoom + type: ConsoleAT.SET_IMAGE_TO_ZOOM +}> + export type ConsoleAction = | ToggleSideMenuAction | setActiveTopPanelAction | setActiveSidebarAction | setActiveBottomPanelAction + | setImageToZoomAction diff --git a/packages/web-console/src/utils/questdb.ts b/packages/web-console/src/utils/questdb.ts index 6e14e72e6..7373ada41 100644 --- a/packages/web-console/src/utils/questdb.ts +++ b/packages/web-console/src/utils/questdb.ts @@ -27,7 +27,7 @@ import { eventBus } from "../modules/EventBus" import { EventType } from "../modules/EventBus/types" import { AuthPayload } from "../modules/OAuth2/types" import { StoreKey } from "./localStorage/types" -import {API_VERSION} from "../consts"; +import { API_VERSION } from "../consts" type ColumnDefinition = Readonly<{ name: string; type: string }> @@ -86,12 +86,12 @@ type RawErrorResult = { } type RawNoticeResult = { - ddl: undefined - dml: undefined - error: undefined - notice: "" - position: undefined - query: string + ddl: undefined + dml: undefined + error: undefined + notice: "" + position: undefined + query: string } type DdlResult = { @@ -104,7 +104,12 @@ type DmlResult = { type: Type.DML } -type RawResult = RawDqlResult | RawDmlResult | RawDdlResult | RawErrorResult | RawNoticeResult +type RawResult = + | RawDqlResult + | RawDmlResult + | RawDdlResult + | RawErrorResult + | RawNoticeResult export type ErrorResult = RawErrorResult & { type: Type.ERROR @@ -112,7 +117,7 @@ export type ErrorResult = RawErrorResult & { } export type NoticeResult = RawNoticeResult & { - type: Type.NOTICE + type: Type.NOTICE } export type QueryRawResult = @@ -620,7 +625,7 @@ export class Client { `chk?${Client.encodeParams({ f: "json", j: name, - version: API_VERSION, + version: API_VERSION, })}`, { headers: this.commonHeaders }, ) From a75269295771567aebcb21a7686c197505a1751f Mon Sep 17 00:00:00 2001 From: Vlad Ilyushchenko Date: Tue, 5 Nov 2024 20:24:27 +0000 Subject: [PATCH 2/4] chore: highlight integer numbers that include `_` (underscore) separator (#353) * chore: highlight integers that include _ as a delimiter * sync master --- packages/browser-tests/questdb | 2 +- .../src/scenes/Editor/Monaco/questdb-sql/language.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/browser-tests/questdb b/packages/browser-tests/questdb index 6442b320c..aa44288ce 160000 --- a/packages/browser-tests/questdb +++ b/packages/browser-tests/questdb @@ -1 +1 @@ -Subproject commit 6442b320c1dd2b17ad9ea0f688600e9d7caf9258 +Subproject commit aa44288ce957601c92df80659bb882a3b8b06292 diff --git a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/language.ts b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/language.ts index f07a59ec7..3ab7f7288 100644 --- a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/language.ts +++ b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/language.ts @@ -109,9 +109,9 @@ export const language: monaco.languages.IMonarchLanguage = { ], ], numbers: [ - [/0[xX][0-9a-fA-F]*/, "number"], - [/[$][+-]*\d*(\.\d*)?/, "number"], - [/((\d+(\.\d*)?)|(\.\d+))([eE][\-+]?\d+)?/, "number"], + [/0[xX][0-9a-fA-F]*/, "number"], // hex integers + [/[+-]?\d+((_)?\d+)*[Ll]?/, "number"], // integers + [/[+-]?\d*(\.\d*)?[Ee]/, "number"], // floating point number ], strings: [ [/N'/, { token: "string", next: "@string" }], From 46af9fb8749c68b02609a161fed2ca0d2b7c9c19 Mon Sep 17 00:00:00 2001 From: glasstiger <94906625+glasstiger@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:41:45 +0000 Subject: [PATCH 3/4] chore(ui): handle and display error received while scrolling the grid (#348) * chore(ui): handle and display error received while scrolling the grid * test * improved test * merge * update submodule * update submodule * update submodule * update submodule --------- Co-authored-by: Vlad Ilyushchenko --- .../cypress/integration/console/grid.spec.js | 13 +++++++ packages/browser-tests/questdb | 2 +- .../web-console/src/scenes/Result/index.tsx | 34 +++++++++++++------ 3 files changed, 38 insertions(+), 11 deletions(-) diff --git a/packages/browser-tests/cypress/integration/console/grid.spec.js b/packages/browser-tests/cypress/integration/console/grid.spec.js index 25460c2eb..933909c5a 100644 --- a/packages/browser-tests/cypress/integration/console/grid.spec.js +++ b/packages/browser-tests/cypress/integration/console/grid.spec.js @@ -59,6 +59,19 @@ describe("questdb grid", () => { cy.getGridViewport().scrollTo("bottom"); }); + it("multiple scrolls till the bottom with error", () => { + const rows = 1200; + cy.typeQuery(`select simulate_crash('P') from long_sequence(${rows})`); + cy.runLine(); + + cy.getGridViewport().scrollTo(0, 999 * rowHeight); + cy.getCollapsedNotifications().should("contain", "1,200 rows in"); + + cy.getGridViewport().scrollTo("bottom"); + cy.wait(100); + cy.getCollapsedNotifications().should("contain", "HTTP 400 (Bad request)"); + }); + it("copy cell into the clipboard", () => { cy.typeQuery("select x from long_sequence(10)"); cy.runLine(); diff --git a/packages/browser-tests/questdb b/packages/browser-tests/questdb index aa44288ce..1620d78e5 160000 --- a/packages/browser-tests/questdb +++ b/packages/browser-tests/questdb @@ -1 +1 @@ -Subproject commit aa44288ce957601c92df80659bb882a3b8b06292 +Subproject commit 1620d78e560d08db2ca8475262fd84879299fcee diff --git a/packages/web-console/src/scenes/Result/index.tsx b/packages/web-console/src/scenes/Result/index.tsx index 843fecb3d..eddb3adce 100644 --- a/packages/web-console/src/scenes/Result/index.tsx +++ b/packages/web-console/src/scenes/Result/index.tsx @@ -24,7 +24,7 @@ import $ from "jquery" import React, { useContext, useEffect, useRef, useState } from "react" -import { useSelector } from "react-redux" +import { useDispatch, useSelector } from "react-redux" import styled from "styled-components" import { Download2, Refresh } from "@styled-icons/remix-line" import { Reset } from "@styled-icons/boxicons-regular" @@ -40,8 +40,8 @@ import { Text, Tooltip, } from "../../components" -import { selectors } from "../../store" -import { color, QueryRawResult } from "../../utils" +import { actions, selectors } from "../../store" +import {color, ErrorResult, QueryRawResult} from "../../utils" import * as QuestDB from "../../utils/questdb" import { ResultViewMode } from "scenes/Console/types" import { Button } from "@questdb/react-components" @@ -49,6 +49,8 @@ import type { IQuestDBGrid } from "../../js/console/grid.js" import { eventBus } from "../../modules/EventBus" import { EventType } from "../../modules/EventBus/types" import { QuestContext } from "../../providers" +import {QueryInNotification} from "../Editor/Monaco/query-in-notification"; +import {NotificationType} from "../../store/Query/types"; const Root = styled.div` display: flex; @@ -98,17 +100,29 @@ const Result = ({ viewMode }: { viewMode: ResultViewMode }) => { const activeSidebar = useSelector(selectors.console.getActiveSidebar) const gridRef = useRef() const [gridFreezeLeftState, setGridFreezeLeftState] = useState(0) - + const dispatch = useDispatch() + useEffect(() => { const _grid = grid( document.getElementById("grid"), async function (sql, lo, hi, rendererFn: (data: QueryRawResult) => void) { - const result = await quest.queryRaw(sql, { - limit: `${lo},${hi}`, - nm: true, - }) - if (result.type === QuestDB.Type.DQL) { - rendererFn(result) + try { + const result = await quest.queryRaw(sql, { + limit: `${lo},${hi}`, + nm: true, + }) + if (result.type === QuestDB.Type.DQL) { + rendererFn(result) + } + } catch (err) { + dispatch(actions.query.stopRunning()) + dispatch( + actions.query.addNotification({ + content: {(err as ErrorResult).error}, + sideContent: , + type: NotificationType.ERROR, + }), + ) } }, ) From 0494e85ef2f996e6b6f4f3b117fea18d1c223618 Mon Sep 17 00:00:00 2001 From: glasstiger <94906625+glasstiger@users.noreply.github.com> Date: Thu, 21 Nov 2024 11:28:16 +0000 Subject: [PATCH 4/4] feat(ui): support for ID token (#355) * feat(ui): support for ID token * handle undefined oidc host * submodule update --- .../cypress/integration/auth/auth.spec.js | 20 ++++--------- packages/browser-tests/questdb | 2 +- .../web-console/src/modules/OAuth2/types.ts | 2 ++ .../web-console/src/modules/OAuth2/utils.ts | 30 +++++++++++-------- .../src/providers/AuthProvider.tsx | 23 +++++++------- .../src/providers/QuestProvider/index.tsx | 8 ++--- .../src/providers/SettingsProvider/types.ts | 1 + .../web-console/src/store/Telemetry/epics.ts | 4 +-- packages/web-console/src/utils/questdb.ts | 4 +-- 9 files changed, 47 insertions(+), 47 deletions(-) diff --git a/packages/browser-tests/cypress/integration/auth/auth.spec.js b/packages/browser-tests/cypress/integration/auth/auth.spec.js index eef68bbdc..d634985c9 100644 --- a/packages/browser-tests/cypress/integration/auth/auth.spec.js +++ b/packages/browser-tests/cypress/integration/auth/auth.spec.js @@ -32,12 +32,10 @@ describe("Auth - UI", () => { "acl.basic.auth.realm.enabled": false, "acl.oidc.enabled": false, "acl.oidc.client.id": null, - "acl.oidc.host": null, - "acl.oidc.port": null, - "acl.oidc.tls.enabled": null, "acl.oidc.authorization.endpoint": null, "acl.oidc.token.endpoint": null, "acl.oidc.pkce.required": null, + "acl.oidc.groups.encoded.in.token": false, }); cy.visit(baseUrl); }); @@ -59,12 +57,10 @@ describe("Auth - OIDC", () => { "acl.basic.auth.realm.enabled": false, "acl.oidc.enabled": true, "acl.oidc.client.id": "test", - "acl.oidc.host": "host", - "acl.oidc.port": 9999, - "acl.oidc.tls.enabled": true, - "acl.oidc.authorization.endpoint": "/auth", - "acl.oidc.token.endpoint": "/token", + "acl.oidc.authorization.endpoint": "https://host:9999/auth", + "acl.oidc.token.endpoint": "https://host:9999/token", "acl.oidc.pkce.required": true, + "acl.oidc.groups.encoded.in.token": false, }); cy.visit(baseUrl); }); @@ -86,12 +82,10 @@ describe("Auth - Basic", () => { "acl.basic.auth.realm.enabled": true, "acl.oidc.enabled": false, "acl.oidc.client.id": null, - "acl.oidc.host": null, - "acl.oidc.port": null, - "acl.oidc.tls.enabled": null, "acl.oidc.authorization.endpoint": null, "acl.oidc.token.endpoint": null, "acl.oidc.pkce.required": null, + "acl.oidc.groups.encoded.in.token": false, }); cy.visit(baseUrl); }); @@ -111,12 +105,10 @@ describe("Auth - Disabled", () => { "acl.basic.auth.realm.enabled": true, "acl.oidc.enabled": false, "acl.oidc.client.id": null, - "acl.oidc.host": null, - "acl.oidc.port": null, - "acl.oidc.tls.enabled": null, "acl.oidc.authorization.endpoint": null, "acl.oidc.token.endpoint": null, "acl.oidc.pkce.required": null, + "acl.oidc.groups.encoded.in.token": false, }); cy.visit(baseUrl); }); diff --git a/packages/browser-tests/questdb b/packages/browser-tests/questdb index 1620d78e5..ad57a7eff 160000 --- a/packages/browser-tests/questdb +++ b/packages/browser-tests/questdb @@ -1 +1 @@ -Subproject commit 1620d78e560d08db2ca8475262fd84879299fcee +Subproject commit ad57a7effc10ba1c9d59db5edd6e0ca7ffca3ebc diff --git a/packages/web-console/src/modules/OAuth2/types.ts b/packages/web-console/src/modules/OAuth2/types.ts index 6e675add8..de7f20a1a 100644 --- a/packages/web-console/src/modules/OAuth2/types.ts +++ b/packages/web-console/src/modules/OAuth2/types.ts @@ -1,7 +1,9 @@ export type AuthPayload = { access_token: string + id_token: string refresh_token: string token_type: string expires_in: number expires_at?: string + groups_encoded_in_token?: boolean } diff --git a/packages/web-console/src/modules/OAuth2/utils.ts b/packages/web-console/src/modules/OAuth2/utils.ts index 8a7836a4b..099cc23ca 100644 --- a/packages/web-console/src/modules/OAuth2/utils.ts +++ b/packages/web-console/src/modules/OAuth2/utils.ts @@ -9,27 +9,33 @@ type TokenPayload = Partial<{ refresh_token: string }> -const getBaseURL = (config: Settings) => { - return `${config["acl.oidc.tls.enabled"] ? "https" : "http"}://${ - config["acl.oidc.host"] - }:${config["acl.oidc.port"]}` +const getBaseURL = (settings: Settings) => { + // if there is no host in settings, no need to construct base URL at all + if (!settings["acl.oidc.host"]) { + return ""; + } + + // if there is host in settings, we are in legacy mode, and we should construct the base URL + return `${settings["acl.oidc.tls.enabled"] ? "https" : "http"}://${ + settings["acl.oidc.host"] + }:${settings["acl.oidc.port"]}` } export const getAuthorisationURL = ({ - config, + settings, code_challenge = null, login, redirect_uri, }: { - config: Settings + settings: Settings code_challenge: string | null login?: boolean redirect_uri: string }) => { const params = { - client_id: config["acl.oidc.client.id"] || "", + client_id: settings["acl.oidc.client.id"] || "", response_type: "code", - scope: config["acl.oidc.scope"] || "openid", + scope: settings["acl.oidc.scope"] || "openid", redirect_uri, } @@ -43,8 +49,8 @@ export const getAuthorisationURL = ({ } return ( - getBaseURL(config) + - config["acl.oidc.authorization.endpoint"] + + getBaseURL(settings) + + settings["acl.oidc.authorization.endpoint"] + "?" + urlParams ) @@ -70,5 +76,5 @@ export const getAuthToken = async ( ) } -export const hasUIAuth = (config: Settings) => - config["acl.enabled"] && !config["acl.basic.auth.realm.enabled"] +export const hasUIAuth = (settings: Settings) => + settings["acl.enabled"] && !settings["acl.basic.auth.realm.enabled"] diff --git a/packages/web-console/src/providers/AuthProvider.tsx b/packages/web-console/src/providers/AuthProvider.tsx index b6783cb16..1b1eae9e3 100644 --- a/packages/web-console/src/providers/AuthProvider.tsx +++ b/packages/web-console/src/providers/AuthProvider.tsx @@ -77,14 +77,13 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { ) const [state, dispatch] = useReducer(reducer, initialState) - const setAuthToken = (tokenResponse: AuthPayload) => { - if (tokenResponse.access_token) { + const setAuthToken = (tokenResponse: AuthPayload, settings: Settings) => { + if (tokenResponse.access_token && tokenResponse.id_token) { + tokenResponse.groups_encoded_in_token = settings["acl.oidc.groups.encoded.in.token"] + tokenResponse.expires_at = getTokenExpirationDate(tokenResponse.expires_in).toString() // convert from the sec offset setValue( StoreKey.AUTH_PAYLOAD, - JSON.stringify({ - ...tokenResponse, - expires_at: getTokenExpirationDate(tokenResponse.expires_in), // convert from the sec offset - }), + JSON.stringify(tokenResponse), ) // if the token payload does not contain the rolling refresh token, we'll keep the old one if (tokenResponse.refresh_token) { @@ -119,7 +118,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { client_id: settings["acl.oidc.client.id"], }) const tokenResponse = await response.json() - setAuthToken(tokenResponse) + setAuthToken(tokenResponse, settings) return tokenResponse } @@ -175,15 +174,15 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { // User is authenticated already if (authPayload !== "") { - const token = JSON.parse(authPayload) + const tokenResponse = JSON.parse(authPayload) // Check if the token expired or is about to in 30 seconds if ( - new Date(token.expires_at).getTime() - Date.now() < 30000 && + new Date(tokenResponse.expires_at).getTime() - Date.now() < 30000 && getValue(StoreKey.AUTH_REFRESH_TOKEN) !== "" ) { await refreshAuthToken(settings) } else { - setSessionData(token) + setSessionData(tokenResponse) } } else { // User has just been redirected back from the OAuth2 provider and has the code @@ -198,7 +197,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { redirect_uri: settings["acl.oidc.redirect.uri"] || window.location.origin + window.location.pathname, }) const tokenResponse = await response.json() - setAuthToken(tokenResponse) + setAuthToken(tokenResponse, settings) } catch (e) { throw e } @@ -253,7 +252,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const code_verifier = generateCodeVerifier(settings) const code_challenge = generateCodeChallenge(code_verifier) window.location.href = getAuthorisationURL({ - config: settings, + settings, code_challenge, login, redirect_uri: settings["acl.oidc.redirect.uri"] || window.location.href, diff --git a/packages/web-console/src/providers/QuestProvider/index.tsx b/packages/web-console/src/providers/QuestProvider/index.tsx index ee1d1a592..8bd7f0d63 100644 --- a/packages/web-console/src/providers/QuestProvider/index.tsx +++ b/packages/web-console/src/providers/QuestProvider/index.tsx @@ -83,7 +83,7 @@ export const QuestProvider = ({ children }: PropsWithChildren) => { const setupClient = async (sessionData: Partial) => { questClient.setCommonHeaders({ - Authorization: `Bearer ${sessionData.access_token}`, + Authorization: `Bearer ${sessionData.groups_encoded_in_token ? sessionData.id_token : sessionData.access_token}`, }) questClient.refreshTokenMethod = () => { @@ -101,11 +101,11 @@ export const QuestProvider = ({ children }: PropsWithChildren) => { }, [sessionData]) useEffect(() => { - const token = getValue(StoreKey.REST_TOKEN) + const restToken = getValue(StoreKey.REST_TOKEN) // User has provided the basic auth credentials - if (token) { + if (restToken) { questClient.setCommonHeaders({ - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${restToken}`, }) void finishAuthCheck() } else { diff --git a/packages/web-console/src/providers/SettingsProvider/types.ts b/packages/web-console/src/providers/SettingsProvider/types.ts index 4c78c2652..81919aed4 100644 --- a/packages/web-console/src/providers/SettingsProvider/types.ts +++ b/packages/web-console/src/providers/SettingsProvider/types.ts @@ -4,6 +4,7 @@ export type Settings = Partial<{ "release.version": string "acl.enabled": boolean "acl.basic.auth.realm.enabled": boolean + "acl.oidc.groups.encoded.in.token": boolean "acl.oidc.enabled": boolean "acl.oidc.client.id": string "acl.oidc.redirect.uri": string diff --git a/packages/web-console/src/store/Telemetry/epics.ts b/packages/web-console/src/store/Telemetry/epics.ts index f8fdf83bf..082a0be24 100644 --- a/packages/web-console/src/store/Telemetry/epics.ts +++ b/packages/web-console/src/store/Telemetry/epics.ts @@ -59,9 +59,9 @@ export const getConfig: Epic = ( ? getValue(StoreKey.AUTH_PAYLOAD) : "{}" const token = JSON.parse(authPayload) as AuthPayload - if (token.access_token) { + if (token.access_token && token.id_token) { quest.setCommonHeaders({ - Authorization: `Bearer ${token.access_token}`, + Authorization: `Bearer ${token.groups_encoded_in_token ? token.id_token : token.access_token}`, }) } else { const restToken = getValue(StoreKey.REST_TOKEN) diff --git a/packages/web-console/src/utils/questdb.ts b/packages/web-console/src/utils/questdb.ts index 7373ada41..9c16ad98a 100644 --- a/packages/web-console/src/utils/questdb.ts +++ b/packages/web-console/src/utils/questdb.ts @@ -344,10 +344,10 @@ export class Client { if (Client.numOfPendingQueries === 0) { clearInterval(interval) const newToken = await this.refreshTokenMethod() - if (newToken.access_token) { + if (newToken.access_token && newToken.id_token) { this.setCommonHeaders({ ...this.commonHeaders, - Authorization: `Bearer ${newToken.access_token}`, + Authorization: `Bearer ${newToken.groups_encoded_in_token ? newToken.id_token : newToken.access_token}`, }) } Client.refreshTokenPending = false