diff --git a/packages/browser-tests/cypress/fixtures/test.csv b/packages/browser-tests/cypress/fixtures/test.csv new file mode 100644 index 000000000..3a0887ac6 --- /dev/null +++ b/packages/browser-tests/cypress/fixtures/test.csv @@ -0,0 +1,3 @@ +date,underlying,name,exchange,symbol,fut_expiration_date,bid,ask,open,high,low,close,volume,open_interest,strike_multiplier,option_price_multiplier +2005-12-01T00:00:00.000000Z,AC,Denatured Fuel Ethanol Futures,CBOT,AC/05Z.CB,1134518400000,0.000000,0.000000,1.900000,1.900000,1.900000,1.900000,2,64,1.000000,1.000000 +2005-12-01T00:00:00.000000Z,AC,Denatured Fuel Ethanol Futures,CBOT,AC/06F.CB,1137110400000,0.000000,0.000000,0.000000,1.865000,1.865000,1.865000,0,83,1.000000,1.000000 diff --git a/packages/browser-tests/cypress/integration/console/import.spec.js b/packages/browser-tests/cypress/integration/console/import.spec.js index 4283fde60..8e85d51e6 100644 --- a/packages/browser-tests/cypress/integration/console/import.spec.js +++ b/packages/browser-tests/cypress/integration/console/import.spec.js @@ -8,5 +8,10 @@ describe("questdb import", () => { it("display import panel", () => { cy.getByDataHook("import-panel-button").click(); cy.getByDataHook("import-dropbox").should("be.visible"); + cy.getByDataHook("import-browse-from-disk").should("be.visible"); + + cy.get('input[type="file"]').selectFile("cypress/fixtures/test.csv", { force: true }); + cy.getByDataHook("import-table-column-schema").should("be.visible"); + cy.getByDataHook("import-table-column-owner").should("not.exist"); }); }); diff --git a/packages/browser-tests/cypress/integration/enterprise/import.spec.js b/packages/browser-tests/cypress/integration/enterprise/import.spec.js new file mode 100644 index 000000000..278299bd6 --- /dev/null +++ b/packages/browser-tests/cypress/integration/enterprise/import.spec.js @@ -0,0 +1,18 @@ +/// + +describe("CSV import in enterprise", () => { + before(() => { + cy.loadConsoleWithAuth(); + }); + + it("display import panel", () => { + cy.getByDataHook("import-panel-button").click(); + cy.getByDataHook("import-dropbox").should("be.visible"); + cy.getByDataHook("import-browse-from-disk").should("be.visible"); + + cy.get('input[type="file"]').selectFile("cypress/fixtures/test.csv", { force: true }); + cy.getByDataHook("import-table-column-schema").should("be.visible"); + cy.getByDataHook("import-table-column-owner").should("be.visible"); + cy.contains("option", "admin").should("exist"); + }); +}); diff --git a/packages/browser-tests/cypress/integration/enterprise/oidc.spec.js b/packages/browser-tests/cypress/integration/enterprise/oidc.spec.js index cd23d1579..c9e3c04af 100644 --- a/packages/browser-tests/cypress/integration/enterprise/oidc.spec.js +++ b/packages/browser-tests/cypress/integration/enterprise/oidc.spec.js @@ -96,4 +96,32 @@ describe("OIDC authentication", () => { cy.getByDataHook("button-log-in").click() cy.getEditor().should("be.visible"); }); + + it("display import panel", () => { + interceptAuthorizationCodeRequest(`${baseUrl}?code=abcdefgh`); + cy.getByDataHook("button-sso-login").click(); + cy.wait("@authorizationCode"); + + interceptTokenRequest({ + "access_token": "gslpJtzmmi6RwaPSx0dYGD4tEkom", + "refresh_token": "FUuAAqMp6LSTKmkUd5uZuodhiE4Kr6M7Eyv", + "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6I", + "token_type": "Bearer", + "expires_in": 300 + }); + cy.wait("@tokens"); + cy.getEditor().should("be.visible"); + + cy.getByDataHook("import-panel-button").click(); + cy.getByDataHook("import-dropbox").should("be.visible"); + cy.getByDataHook("import-browse-from-disk").should("be.visible"); + + cy.get('input[type="file"]').selectFile("cypress/fixtures/test.csv", { force: true }); + cy.getByDataHook("import-table-column-schema").should("be.visible"); + cy.getByDataHook("import-table-column-owner").should("be.visible"); + cy.contains("option", "user1").should("not.exist"); + cy.contains("option", "group1").should("exist"); + + cy.logout(); + }); }); diff --git a/packages/web-console/serve-dist.js b/packages/web-console/serve-dist.js index 456e7301a..5cf46c7dc 100644 --- a/packages/web-console/serve-dist.js +++ b/packages/web-console/serve-dist.js @@ -10,7 +10,7 @@ const server = http.createServer((req, res) => { res.statusCode = 403; res.end(); return; - } + } const { method } = req const baseUrl = "http://" + req.headers.host + contextPath; const reqUrl = new url.URL(req.url, baseUrl); @@ -20,7 +20,8 @@ const server = http.createServer((req, res) => { if ( reqPathName.startsWith("/exec") || reqPathName.startsWith("/settings") || - reqPathName.startsWith("/warnings") + reqPathName.startsWith("/warnings") || + reqPathName.startsWith("/chk") ) { // proxy /exec requests to localhost:9000 const options = { diff --git a/packages/web-console/src/scenes/Import/ImportCSVFiles/files-to-upload.tsx b/packages/web-console/src/scenes/Import/ImportCSVFiles/files-to-upload.tsx index 759f81fbc..120cb1654 100644 --- a/packages/web-console/src/scenes/Import/ImportCSVFiles/files-to-upload.tsx +++ b/packages/web-console/src/scenes/Import/ImportCSVFiles/files-to-upload.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useRef } from "react" import styled from "styled-components" import { Heading, Table, Select, Button } from "@questdb/react-components" -import type { Props as TableProps } from "@questdb/react-components/dist/components/Table" +import { Column, Props as TableProps } from "@questdb/react-components/dist/components/Table" import { PopperHover, Text, Tooltip } from "../../../components" import { Box } from "../../../components/Box" import { bytesWithSuffix } from "../../../utils/bytesWithSuffix" @@ -78,6 +78,7 @@ type Props = { onFilesDropped: (files: File[]) => void onViewData: (result: UploadResult) => void dialogOpen: boolean + ownedByList: string[] } export const FilesToUpload = ({ @@ -89,6 +90,7 @@ export const FilesToUpload = ({ onFilesDropped, onViewData, dialogOpen, + ownedByList, }: Props) => { const uploadInputRef = useRef(null) const [renameDialogOpen, setRenameDialogOpen] = React.useState< @@ -105,6 +107,262 @@ export const FilesToUpload = ({ ) }, [renameDialogOpen, schemaDialogOpen]) + const columns: Column[] = [] + columns.push( + { + header: "File", + align: "flex-start", + ...(files.length > 0 && { width: "400px" }), + render: ({ data }) => { + const file = ( + + + {shortenText(data.fileObject.name, 20)} + + + + {bytesWithSuffix(data.fileObject.size)} + + + ) + return ( + + + + {data.fileObject.name.length > 20 && ( + + {data.fileObject.name} + + )} + {data.fileObject.name.length <= 20 && file} + + + {!data.isUploading && + data.uploadResult !== undefined && ( + + + + + )} + + {(data.uploadResult && + data.uploadResult.rowsRejected > 0) || + (data.error && ( + + {data.uploadResult && + data.uploadResult.rowsRejected > 0 && ( + + {data.uploadResult.rowsRejected.toLocaleString()}{" "} + row + {data.uploadResult.rowsRejected > 1 + ? "s" + : ""}{" "} + rejected + + )} + {data.error && ( + + {data.error} + + )} + + ))} + + + ) + }, + }, + { + header: "Table name", + align: "flex-end", + width: "180px", + render: ({ data }) => { + return ( + setRenameDialogOpen(f?.id)} + onNameChange={(name) => { + onFilePropertyChange(data.id, { + table_name: name, + }) + }} + file={data} + /> + ) + }, + }, + ) + + if (ownedByList && ownedByList.length > 0) { + columns.push( + { + header: ( + + Table owner + + + } + > + + Required for external (non-database) users. + + + ), + align: "center", + width: "150px", + render: ({ data }) => ( + ) => + onFilePropertyChange(data.id, { + settings: { + ...data.settings, + overwrite: e.target.value === "true", + }, + }) + } + options={[ + { + label: "Append", + value: "false", + }, + { + label: "Overwrite", + value: "true", + }, + ]} + /> + ), + }, + { + align: "flex-end", + width: "300px", + render: ({ data }) => ( + { + onFilePropertyChange(data.id, { + settings, + }) + }} + /> + ), + }, + ) + return ( )} >> - columns={[ - { - header: "File", - align: "flex-start", - ...(files.length > 0 && { width: "400px" }), - render: ({ data }) => { - const file = ( - - - {shortenText(data.fileObject.name, 20)} - - - - {bytesWithSuffix(data.fileObject.size)} - - - ) - return ( - - - - {data.fileObject.name.length > 20 && ( - - {data.fileObject.name} - - )} - {data.fileObject.name.length <= 20 && file} - - - {!data.isUploading && - data.uploadResult !== undefined && ( - - - - - )} - - {(data.uploadResult && - data.uploadResult.rowsRejected > 0) || - (data.error && ( - - {data.uploadResult && - data.uploadResult.rowsRejected > 0 && ( - - {data.uploadResult.rowsRejected.toLocaleString()}{" "} - row - {data.uploadResult.rowsRejected > 1 - ? "s" - : ""}{" "} - rejected - - )} - {data.error && ( - - {data.error} - - )} - - ))} - - - ) - }, - }, - { - header: "Table name", - align: "flex-end", - width: "200px", - render: ({ data }) => { - return ( - setRenameDialogOpen(f?.id)} - onNameChange={(name) => { - onFilePropertyChange(data.id, { - table_name: name, - }) - }} - file={data} - /> - ) - }, - }, - { - header: ( - - Schema - - - } - > - - Optional. By default, QuestDB will infer schema from the - CSV file structure - - - ), - - align: "center", - width: "150px", - render: ({ data }) => { - const name = data.table_name ?? data.fileObject.name - return ( - - setSchemaDialogOpen(name ? data.id : undefined) - } - onSchemaChange={(schema) => { - onFilePropertyChange(data.id, { - schema: schema.schemaColumns, - partitionBy: schema.partitionBy, - timestamp: schema.timestamp, - }) - }} - name={name} - schema={data.schema} - partitionBy={data.partitionBy} - ttlValue={data.ttlValue} - ttlUnit={data.ttlUnit} - timestamp={data.timestamp} - isEditLocked={ - data.exists && data.table_name === data.fileObject.name - } - hasWalSetting={false} - ctaText="Save" - /> - ) - }, - }, - { - header: ( - - Write mode - - - } - > - - Append: data will be appended to the set. -
- Overwrite: any existing data or structure - will be overwritten. Required for partitioning and - timestamp related changes. -
-
- ), - align: "center", - width: "150px", - render: ({ data }) => ( -