Skip to content

Commit b1b6a9d

Browse files
emrberkideomaglasstiger
authored
feat(web-console): export parquet files, fix unresponsive CSV imports (#484)
* export parquet initial * feat(web-console): export parquet files, fix unresponsive CSV imports * update submodule and loader text * handle safari downloads with new tab, fix token handling in service worker controller change * fix type and quotes * more on reviews * show parquet download as the default action * remove icons * adjust line height of the text * animation * simplify the auth flow, add fallbacks * add nodelay request param * convert download flow to use link directly * fix arrow direction * handle error body in iframe * submodule * chore(ui): HTTP session (#489) * chore(ui): HTTP session * chore(browser-tests): add session tests * submodule --------- Co-authored-by: emrberk <emreberk5699@gmail.com> Co-authored-by: Emre Berk Kaya <75899391+emrberk@users.noreply.github.com> * update test * submodule --------- Co-authored-by: ideoma <2159629+ideoma@users.noreply.github.com> Co-authored-by: glasstiger <94906625+glasstiger@users.noreply.github.com>
1 parent dd239f8 commit b1b6a9d

File tree

13 files changed

+437
-64
lines changed

13 files changed

+437
-64
lines changed

packages/browser-tests/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
cypress/videos
33
cypress/screenshots
4+
cypress/downloads

packages/browser-tests/cypress/integration/auth/auth.spec.js

Lines changed: 83 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="cypress" />
22

3-
const contextPath = process.env.QDB_HTTP_CONTEXT_WEB_CONSOLE || ""
3+
const contextPath = process.env.QDB_HTTP_CONTEXT_WEB_CONSOLE || "";
44
const baseUrl = `http://localhost:9999${contextPath}`;
55

66
const interceptSettings = (payload) => {
@@ -12,10 +12,10 @@ const interceptSettings = (payload) => {
1212
describe("OSS", () => {
1313
before(() => {
1414
interceptSettings({
15-
"config": {
15+
config: {
1616
"release.type": "OSS",
1717
"release.version": "1.2.3",
18-
}
18+
},
1919
});
2020
cy.visit(baseUrl);
2121
});
@@ -29,7 +29,7 @@ describe("OSS", () => {
2929
describe("Auth - UI", () => {
3030
before(() => {
3131
interceptSettings({
32-
"config": {
32+
config: {
3333
"release.type": "EE",
3434
"release.version": "1.2.3",
3535
"acl.enabled": true,
@@ -40,7 +40,7 @@ describe("Auth - UI", () => {
4040
"acl.oidc.token.endpoint": null,
4141
"acl.oidc.pkce.required": null,
4242
"acl.oidc.groups.encoded.in.token": false,
43-
}
43+
},
4444
});
4545
cy.visit(baseUrl);
4646
});
@@ -52,11 +52,10 @@ describe("Auth - UI", () => {
5252
});
5353
});
5454

55-
5655
describe("Auth - OIDC", () => {
5756
before(() => {
5857
interceptSettings({
59-
"config": {
58+
config: {
6059
"release.type": "EE",
6160
"release.version": "1.2.3",
6261
"acl.enabled": true,
@@ -67,7 +66,7 @@ describe("Auth - OIDC", () => {
6766
"acl.oidc.token.endpoint": "https://host:9999/token",
6867
"acl.oidc.pkce.required": true,
6968
"acl.oidc.groups.encoded.in.token": false,
70-
}
69+
},
7170
});
7271
cy.visit(baseUrl);
7372
});
@@ -83,7 +82,7 @@ describe("Auth - OIDC", () => {
8382
describe("Auth - Basic", () => {
8483
before(() => {
8584
interceptSettings({
86-
"config": {
85+
config: {
8786
"release.type": "EE",
8887
"release.version": "1.2.3",
8988
"acl.enabled": true,
@@ -94,7 +93,7 @@ describe("Auth - Basic", () => {
9493
"acl.oidc.token.endpoint": null,
9594
"acl.oidc.pkce.required": null,
9695
"acl.oidc.groups.encoded.in.token": false,
97-
}
96+
},
9897
});
9998
cy.visit(baseUrl);
10099
});
@@ -108,7 +107,7 @@ describe("Auth - Basic", () => {
108107
describe("Auth - Disabled", () => {
109108
before(() => {
110109
interceptSettings({
111-
"config": {
110+
config: {
112111
"release.type": "EE",
113112
"release.version": "1.2.3",
114113
"acl.enabled": false,
@@ -119,7 +118,7 @@ describe("Auth - Disabled", () => {
119118
"acl.oidc.token.endpoint": null,
120119
"acl.oidc.pkce.required": null,
121120
"acl.oidc.groups.encoded.in.token": false,
122-
}
121+
},
123122
});
124123
cy.visit(baseUrl);
125124
});
@@ -129,3 +128,75 @@ describe("Auth - Disabled", () => {
129128
cy.getEditor().should("be.visible");
130129
});
131130
});
131+
132+
describe("Auth - Session Parameter (OAuth)", () => {
133+
describe("OAuth Login with session=true", () => {
134+
beforeEach(() => {
135+
interceptSettings({
136+
config: {
137+
"release.type": "EE",
138+
"release.version": "1.2.3",
139+
"acl.enabled": true,
140+
"acl.basic.auth.realm.enabled": false,
141+
"acl.oidc.enabled": true,
142+
"acl.oidc.client.id": "test-client",
143+
"acl.oidc.authorization.endpoint": "https://oauth.example.com/auth",
144+
"acl.oidc.token.endpoint": "https://oauth.example.com/token",
145+
"acl.oidc.pkce.required": true,
146+
"acl.oidc.groups.encoded.in.token": false,
147+
},
148+
});
149+
});
150+
151+
it("should call exec with session=true after OAuth token exchange", () => {
152+
cy.intercept(
153+
{
154+
method: "GET",
155+
url: `${baseUrl}/exec?query=select%202&session=true`,
156+
},
157+
(req) => {
158+
expect(req.headers).to.have.property("authorization");
159+
expect(req.headers.authorization).to.match(/^Bearer /);
160+
161+
req.reply({
162+
statusCode: 200,
163+
headers: {
164+
"set-cookie": "qdb-session=oauth-session-id; Path=/; HttpOnly",
165+
},
166+
body: {
167+
query: "select 2",
168+
columns: [{ name: "column", type: "INT" }],
169+
dataset: [[2]],
170+
count: 1,
171+
},
172+
});
173+
}
174+
).as("oauthSessionStart");
175+
176+
cy.intercept(
177+
{
178+
method: "POST",
179+
url: "https://oauth.example.com/token",
180+
},
181+
{
182+
statusCode: 200,
183+
body: {
184+
access_token: "mock-access-token",
185+
token_type: "Bearer",
186+
expires_in: 3600,
187+
},
188+
}
189+
).as("tokenExchange");
190+
191+
cy.visit(`${baseUrl}?code=test-auth-code&state=test-state`);
192+
cy.wait("@settings");
193+
194+
cy.wait("@tokenExchange");
195+
cy.wait("@oauthSessionStart").then((interception) => {
196+
expect(interception.request.url).to.include("session=true");
197+
expect(interception.request.url).to.include("select%202");
198+
expect(interception.response.headers).to.have.property("set-cookie");
199+
});
200+
});
201+
});
202+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
describe("download functionality", () => {
2+
beforeEach(() => {
3+
cy.loadConsoleWithAuth();
4+
});
5+
6+
it("should show download button with results", () => {
7+
// When
8+
cy.typeQuery("select x from long_sequence(10)");
9+
cy.runLine();
10+
11+
// Then
12+
cy.getByDataHook("download-parquet-button").should("be.visible");
13+
cy.getByDataHook("download-dropdown-button").should("be.visible");
14+
cy.getByDataHook("download-csv-button").should("not.exist");
15+
16+
// When
17+
cy.getByDataHook("download-dropdown-button").click();
18+
19+
// Then
20+
cy.getByDataHook("download-csv-button").should("be.visible");
21+
});
22+
23+
it("should trigger CSV download", () => {
24+
const query = "select x from long_sequence(10)";
25+
26+
// Given
27+
cy.intercept("GET", "**/exp?*", (req) => {
28+
req.reply({
29+
statusCode: 200,
30+
body: null,
31+
});
32+
}).as("exportRequest");
33+
34+
// When
35+
cy.typeQuery(query);
36+
cy.runLine();
37+
cy.getByDataHook("download-dropdown-button").click();
38+
cy.getByDataHook("download-csv-button").click();
39+
40+
// Then
41+
cy.wait("@exportRequest").then((interception) => {
42+
expect(interception.request.url).to.include("fmt=csv");
43+
expect(interception.request.url).to.include(
44+
encodeURIComponent(query.replace(/\s+/g, " "))
45+
);
46+
});
47+
});
48+
49+
it("should trigger Parquet download", () => {
50+
const query = "select x from long_sequence(10)";
51+
52+
// Given
53+
cy.intercept("GET", "**/exp?*", (req) => {
54+
req.reply({
55+
statusCode: 200,
56+
body: null,
57+
});
58+
}).as("exportRequest");
59+
60+
// When
61+
cy.typeQuery(query);
62+
cy.runLine();
63+
cy.getByDataHook("download-parquet-button").click();
64+
65+
// Then
66+
cy.wait("@exportRequest").then((interception) => {
67+
expect(interception.request.url).to.include("fmt=parquet");
68+
expect(interception.request.url).to.include("rmode=nodelay");
69+
expect(interception.request.url).to.include(
70+
encodeURIComponent(query.replace(/\s+/g, " "))
71+
);
72+
});
73+
});
74+
75+
it("should show error toast on bad request", () => {
76+
// Given
77+
cy.intercept("GET", "**/exp?*", (req) => {
78+
const url = new URL(req.url);
79+
url.searchParams.set("query", "badquery");
80+
req.url = url.toString();
81+
}).as("badExportRequest");
82+
83+
// When
84+
cy.typeQuery("select x from long_sequence(5)");
85+
cy.runLine();
86+
cy.getByDataHook("download-dropdown-button").click();
87+
cy.getByDataHook("download-csv-button").click();
88+
89+
// Then
90+
cy.wait("@badExportRequest").then(() => {
91+
cy.getByRole("alert").should(
92+
"contain",
93+
"An error occurred while downloading the file: table does not exist [table=badquery]"
94+
);
95+
});
96+
});
97+
});
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/// <reference types="cypress" />
2+
3+
const contextPath = process.env.QDB_HTTP_CONTEXT_WEB_CONSOLE || "";
4+
const baseUrl = `http://localhost:9999${contextPath}`;
5+
6+
describe("HTTP Session Management", () => {
7+
it("should create session on login, maintain it across requests, and persist after page refresh", () => {
8+
// Given
9+
cy.intercept("GET", `${baseUrl}/exec?query=*&session=true`).as(
10+
"sessionStart"
11+
);
12+
13+
// When
14+
cy.handleStorageAndVisit(baseUrl);
15+
cy.loginWithUserAndPassword();
16+
17+
// Then
18+
cy.wait("@sessionStart").then((interception) => {
19+
expect(interception.request.url).to.include("session=true");
20+
expect(interception.request.headers).to.have.property("authorization");
21+
expect(interception.response.headers["set-cookie"]).to.exist;
22+
});
23+
cy.getEditor().should("be.visible");
24+
25+
// Given
26+
cy.intercept("GET", /\/exec\?.*query=SELECT%201/).as("queryExec");
27+
28+
// When
29+
cy.clearEditor();
30+
cy.typeQuery("SELECT 1");
31+
cy.clickRunIconInLine(1);
32+
33+
// Then
34+
cy.wait("@queryExec").then((interception) => {
35+
expect(interception.request.url).to.not.include("session=true");
36+
expect(interception.request.url).to.not.include("session=false");
37+
expect(interception.response.statusCode).to.equal(200);
38+
});
39+
cy.getGrid().should("be.visible");
40+
41+
// When
42+
cy.handleStorageAndVisit(baseUrl, false);
43+
44+
// Then
45+
cy.getEditor().should("be.visible");
46+
cy.clearEditor();
47+
cy.typeQuery("SELECT 2");
48+
cy.clickRunIconInLine(1);
49+
cy.getGrid().should("be.visible");
50+
cy.getGridRow(0).should("contain", "2");
51+
});
52+
53+
it("should destroy session on logout, clear local storage, and show login screen after refresh", () => {
54+
// Given
55+
cy.loadConsoleWithAuth();
56+
cy.window().then((win) => {
57+
const basicAuthHeader = win.localStorage.getItem("basic.auth.header");
58+
expect(basicAuthHeader).to.not.be.null;
59+
});
60+
cy.intercept("GET", `${baseUrl}/exec?query=*&session=false`).as(
61+
"sessionDestroy"
62+
);
63+
64+
// When
65+
cy.getByDataHook("button-logout").click();
66+
67+
// Then
68+
cy.wait("@sessionDestroy").then((interception) => {
69+
expect(interception.request.url).to.include("session=false");
70+
expect(interception.request.url).to.include("select%202");
71+
});
72+
cy.getByDataHook("auth-login").should("be.visible");
73+
cy.window().then((win) => {
74+
const basicAuthHeader = win.localStorage.getItem("basic.auth.header");
75+
const restToken = win.localStorage.getItem("rest.token");
76+
expect(basicAuthHeader).to.be.null;
77+
expect(restToken).to.be.null;
78+
});
79+
80+
// When
81+
cy.reload();
82+
83+
// Then
84+
cy.getByDataHook("auth-login").should("be.visible");
85+
cy.getEditor().should("not.exist");
86+
});
87+
});

packages/browser-tests/questdb

Submodule questdb updated 532 files

packages/web-console/serve-dist.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ const server = http.createServer((req, res) => {
2222
reqPathName.startsWith("/settings") ||
2323
reqPathName.startsWith("/warnings") ||
2424
reqPathName.startsWith("/chk") ||
25-
reqPathName.startsWith("/imp")
25+
reqPathName.startsWith("/imp") ||
26+
reqPathName.startsWith("/exp")
2627
) {
2728
// proxy /exec requests to localhost:9000
2829
const options = {

0 commit comments

Comments
 (0)