diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a0f362657..9dd6d7d85 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -30,6 +30,8 @@ jobs:
- name: Start QuestDB
run: ./tmp/questdb-*-rt-linux-amd64/bin/questdb.sh start -d ./tmp/dbroot
+ env:
+ QDB_DEV_MODE_ENABLED: "true"
- uses: actions/setup-node@v4
with:
diff --git a/.pnp.cjs b/.pnp.cjs
index f6683d490..60f56ba1f 100755
--- a/.pnp.cjs
+++ b/.pnp.cjs
@@ -6578,6 +6578,7 @@ const RAW_RUNTIME_STATE =
["@types/ramda", "npm:0.27.40"],\
["@types/react", "npm:17.0.2"],\
["@types/react-dom", "npm:16.9.8"],\
+ ["@types/react-highlight-words", "npm:0.16.7"],\
["@types/react-redux", "npm:7.1.9"],\
["@types/react-transition-group", "npm:4.4.0"],\
["@types/styled-components", "npm:5.1.7"],\
@@ -6635,6 +6636,7 @@ const RAW_RUNTIME_STATE =
["react", "npm:17.0.2"],\
["react-contextmenu", "virtual:b7c775051d99785ec73273707ec685f06b84c737b39cc023ebc60bda25254288f27e86643fb15b7a00bd8a6d76cc119d4628443d858e76eb62af2718d7eb18cf#npm:2.14.0"],\
["react-dom", "virtual:455cc1315669a6e622038e6093381f8d95ab8bb473af09abaaf7e72fce2d6a9ff2602fe53215abe1948a8259d689b4411a17531ea4acc5522a659c642ee7696d#npm:17.0.2"],\
+ ["react-highlight-words", "virtual:b7c775051d99785ec73273707ec685f06b84c737b39cc023ebc60bda25254288f27e86643fb15b7a00bd8a6d76cc119d4628443d858e76eb62af2718d7eb18cf#npm:0.20.0"],\
["react-hook-form", "virtual:b7c775051d99785ec73273707ec685f06b84c737b39cc023ebc60bda25254288f27e86643fb15b7a00bd8a6d76cc119d4628443d858e76eb62af2718d7eb18cf#npm:7.22.3"],\
["react-is", "npm:18.2.0"],\
["react-markdown", "virtual:b7c775051d99785ec73273707ec685f06b84c737b39cc023ebc60bda25254288f27e86643fb15b7a00bd8a6d76cc119d4628443d858e76eb62af2718d7eb18cf#npm:8.0.7"],\
@@ -11041,6 +11043,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["@types/react-highlight-words", [\
+ ["npm:0.16.7", {\
+ "packageLocation": "./.yarn/cache/@types-react-highlight-words-npm-0.16.7-e9c0694a2e-503e49f396.zip/node_modules/@types/react-highlight-words/",\
+ "packageDependencies": [\
+ ["@types/react-highlight-words", "npm:0.16.7"],\
+ ["@types/react", "npm:17.0.2"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["@types/react-redux", [\
["npm:7.1.9", {\
"packageLocation": "./.yarn/cache/@types-react-redux-npm-7.1.9-f1ca2ff600-63f5a9dc02.zip/node_modules/@types/react-redux/",\
@@ -19003,6 +19015,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["highlight-words-core", [\
+ ["npm:1.2.2", {\
+ "packageLocation": "./.yarn/cache/highlight-words-core-npm-1.2.2-b168c62a9a-6a5364ad8c.zip/node_modules/highlight-words-core/",\
+ "packageDependencies": [\
+ ["highlight-words-core", "npm:1.2.2"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["hoist-non-react-statics", [\
["npm:3.3.2", {\
"packageLocation": "./.yarn/cache/hoist-non-react-statics-npm-3.3.2-e7b709e6c1-1acbe85f33.zip/node_modules/hoist-non-react-statics/",\
@@ -22273,6 +22294,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["memoize-one", [\
+ ["npm:4.0.3", {\
+ "packageLocation": "./.yarn/cache/memoize-one-npm-4.0.3-6aef1020af-6aee6da4d5.zip/node_modules/memoize-one/",\
+ "packageDependencies": [\
+ ["memoize-one", "npm:4.0.3"]\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["memoizerific", [\
["npm:1.11.3", {\
"packageLocation": "./.yarn/cache/memoizerific-npm-1.11.3-3cd7adb7ec-72b6b80699.zip/node_modules/memoizerific/",\
@@ -25142,6 +25172,31 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\
}]\
]],\
+ ["react-highlight-words", [\
+ ["npm:0.20.0", {\
+ "packageLocation": "./.yarn/cache/react-highlight-words-npm-0.20.0-02e799bf4d-5adf2cfb1f.zip/node_modules/react-highlight-words/",\
+ "packageDependencies": [\
+ ["react-highlight-words", "npm:0.20.0"]\
+ ],\
+ "linkType": "SOFT"\
+ }],\
+ ["virtual:b7c775051d99785ec73273707ec685f06b84c737b39cc023ebc60bda25254288f27e86643fb15b7a00bd8a6d76cc119d4628443d858e76eb62af2718d7eb18cf#npm:0.20.0", {\
+ "packageLocation": "./.yarn/__virtual__/react-highlight-words-virtual-982d56fbbb/0/cache/react-highlight-words-npm-0.20.0-02e799bf4d-5adf2cfb1f.zip/node_modules/react-highlight-words/",\
+ "packageDependencies": [\
+ ["react-highlight-words", "virtual:b7c775051d99785ec73273707ec685f06b84c737b39cc023ebc60bda25254288f27e86643fb15b7a00bd8a6d76cc119d4628443d858e76eb62af2718d7eb18cf#npm:0.20.0"],\
+ ["@types/react", "npm:17.0.2"],\
+ ["highlight-words-core", "npm:1.2.2"],\
+ ["memoize-one", "npm:4.0.3"],\
+ ["prop-types", "npm:15.8.1"],\
+ ["react", "npm:17.0.2"]\
+ ],\
+ "packagePeers": [\
+ "@types/react",\
+ "react"\
+ ],\
+ "linkType": "HARD"\
+ }]\
+ ]],\
["react-hook-form", [\
["npm:7.22.3", {\
"packageLocation": "./.yarn/cache/react-hook-form-npm-7.22.3-68164760a3-8b3a22b91d.zip/node_modules/react-hook-form/",\
diff --git a/.yarn/cache/@types-react-highlight-words-npm-0.16.7-e9c0694a2e-503e49f396.zip b/.yarn/cache/@types-react-highlight-words-npm-0.16.7-e9c0694a2e-503e49f396.zip
new file mode 100644
index 000000000..ddf8abdcb
Binary files /dev/null and b/.yarn/cache/@types-react-highlight-words-npm-0.16.7-e9c0694a2e-503e49f396.zip differ
diff --git a/.yarn/cache/highlight-words-core-npm-1.2.2-b168c62a9a-6a5364ad8c.zip b/.yarn/cache/highlight-words-core-npm-1.2.2-b168c62a9a-6a5364ad8c.zip
new file mode 100644
index 000000000..c46bfa16c
Binary files /dev/null and b/.yarn/cache/highlight-words-core-npm-1.2.2-b168c62a9a-6a5364ad8c.zip differ
diff --git a/.yarn/cache/memoize-one-npm-4.0.3-6aef1020af-6aee6da4d5.zip b/.yarn/cache/memoize-one-npm-4.0.3-6aef1020af-6aee6da4d5.zip
new file mode 100644
index 000000000..c98a59c49
Binary files /dev/null and b/.yarn/cache/memoize-one-npm-4.0.3-6aef1020af-6aee6da4d5.zip differ
diff --git a/.yarn/cache/react-highlight-words-npm-0.20.0-02e799bf4d-5adf2cfb1f.zip b/.yarn/cache/react-highlight-words-npm-0.20.0-02e799bf4d-5adf2cfb1f.zip
new file mode 100644
index 000000000..6bf1baefa
Binary files /dev/null and b/.yarn/cache/react-highlight-words-npm-0.20.0-02e799bf4d-5adf2cfb1f.zip differ
diff --git a/packages/browser-tests/cypress.config.js b/packages/browser-tests/cypress.config.js
index af92810a5..7b46addc0 100644
--- a/packages/browser-tests/cypress.config.js
+++ b/packages/browser-tests/cypress.config.js
@@ -4,6 +4,7 @@ const {
} = require("@simonsmith/cypress-image-snapshot/plugin");
module.exports = defineConfig({
+ defaultCommandTimeout: 10000,
e2e: {
defaultCommandTimeout: 30000,
screenshotOnRunFailure: false,
diff --git a/packages/browser-tests/cypress/commands.js b/packages/browser-tests/cypress/commands.js
index 96dec7d64..d360a8898 100644
--- a/packages/browser-tests/cypress/commands.js
+++ b/packages/browser-tests/cypress/commands.js
@@ -13,6 +13,17 @@ const { ctrlOrCmd, escapeRegExp } = require("./utils");
const baseUrl = "http://localhost:9999";
+const tableSchemas = {
+ btc_trades:
+ "CREATE TABLE IF NOT EXISTS 'btc_trades' (symbol SYMBOL capacity 256 CACHE, side SYMBOL capacity 256 CACHE, price DOUBLE, amount DOUBLE, timestamp TIMESTAMP) timestamp (timestamp) PARTITION BY DAY WAL DEDUP UPSERT KEYS(symbol, price, amount, timestamp);",
+ chicago_weather_stations:
+ "CREATE TABLE IF NOT EXISTS 'chicago_weather_stations' (MeasurementTimestamp TIMESTAMP, StationName SYMBOL capacity 256 CACHE, AirTemperature DOUBLE, WetBulbTemperature DOUBLE, Humidity INT, RainIntensity DOUBLE, IntervalRain DOUBLE, TotalRain DOUBLE, PrecipitationType INT, WindDirection INT, WindSpeed DOUBLE, MaximumWindSpeed DOUBLE, BarometricPressure DOUBLE, SolarRadiation INT, Heading INT, BatteryLife DOUBLE, MeasurementTimestampLabel STRING, MeasurementID STRING) timestamp (MeasurementTimestamp) PARTITION BY MONTH WAL DEDUP UPSERT KEYS(MeasurementTimestamp, StationName);",
+ ecommerce_stats:
+ "CREATE TABLE IF NOT EXISTS 'ecommerce_stats' (ts TIMESTAMP, country SYMBOL capacity 256 CACHE, category SYMBOL capacity 256 CACHE, visits LONG, unique_visitors LONG, sales DOUBLE, number_of_products INT) timestamp (ts) PARTITION BY DAY WAL DEDUP UPSERT KEYS(ts, country, category);",
+ gitlog:
+ "CREATE TABLE IF NOT EXISTS 'gitlog' (committed_datetime TIMESTAMP, repo SYMBOL capacity 256 CACHE, author_name SYMBOL capacity 256 CACHE, summary STRING, size INT, insertions INT, deletions INT, lines INT, files INT) timestamp (committed_datetime) PARTITION BY MONTH WAL DEDUP UPSERT KEYS(committed_datetime, repo, author_name);",
+};
+
before(() => {
Cypress.on("uncaught:exception", (err) => {
// this error can be safely ignored
@@ -57,6 +68,7 @@ beforeEach(() => {
req.reply("{}");
}
).as("addTelemetry");
+
cy.intercept(
{
method: "GET",
@@ -67,8 +79,17 @@ beforeEach(() => {
req.reply("[]");
}
);
+
+ cy.request({
+ method: "GET",
+ url: `${baseUrl}/exec?query=${encodeURIComponent(`select simulate_warnings('', '');`)}`,
+ });
});
+Cypress.Commands.add("getByDataHook", (name) =>
+ cy.get(`[data-hook="${name}"]`)
+);
+
Cypress.Commands.add("getGrid", () =>
cy.get(".qg-viewport .qg-canvas").should("be.visible")
);
@@ -95,10 +116,11 @@ Cypress.Commands.add("runLine", () => {
cy.wait("@exec");
});
-Cypress.Commands.add("clickRun", () => {
- cy.intercept("/exec*").as("exec");
- return cy.get("button").contains("Run").click().wait("@exec");
-});
+Cypress.Commands.add("clickRun",
+ () => {
+ cy.intercept("/exec*").as("exec");
+ return cy.get("button").contains("Run").click().wait("@exec");
+ });
Cypress.Commands.add("clearEditor", () =>
cy.typeQuery(`${ctrlOrCmd}a{backspace}`)
@@ -172,6 +194,20 @@ Cypress.Commands.add("getExpandedNotifications", () =>
cy.get('[data-hook="notifications-expanded"]')
);
+Cypress.Commands.add("createTable", (name) => {
+ cy.request({
+ method: "GET",
+ url: `${baseUrl}/exec?query=${encodeURIComponent(tableSchemas[name])};`,
+ });
+});
+
+Cypress.Commands.add("dropTable", (name) => {
+ cy.request({
+ method: "GET",
+ url: `${baseUrl}/exec?query=${encodeURIComponent(`DROP TABLE ${name};`)}`,
+ });
+});
+
Cypress.Commands.add("interceptQuery", (query, alias, response) => {
cy.intercept(
{
diff --git a/packages/browser-tests/cypress/integration/console/auth.spec.js b/packages/browser-tests/cypress/integration/console/auth.spec.js
index 3e63d3795..eef68bbdc 100644
--- a/packages/browser-tests/cypress/integration/console/auth.spec.js
+++ b/packages/browser-tests/cypress/integration/console/auth.spec.js
@@ -44,7 +44,7 @@ describe("Auth - UI", () => {
it("should display UI auth", () => {
cy.wait("@settings");
- cy.get('[data-hook="auth-login"]').should("be.visible");
+ cy.getByDataHook("auth-login").should("be.visible");
cy.getEditor().should("not.exist");
});
});
@@ -71,8 +71,8 @@ describe("Auth - OIDC", () => {
it("should display UI auth with OIDC support", () => {
cy.wait("@settings");
- cy.get('[data-hook="auth-login"]').should("be.visible");
- cy.get('[data-hook="button-sso-login"]').should("be.visible");
+ cy.getByDataHook("auth-login").should("be.visible");
+ cy.getByDataHook("button-sso-login").should("be.visible");
cy.getEditor().should("not.exist");
});
});
diff --git a/packages/browser-tests/cypress/integration/console/grid.spec.js b/packages/browser-tests/cypress/integration/console/grid.spec.js
index 837c478c6..7ff4ffade 100644
--- a/packages/browser-tests/cypress/integration/console/grid.spec.js
+++ b/packages/browser-tests/cypress/integration/console/grid.spec.js
@@ -20,10 +20,8 @@ describe("questdb grid", () => {
cy.runLine();
cy.wait(100);
- cy.getGridRows()
- .should("have.length", 7)
- .getGridRow(0)
- .should("contain", "1");
+ cy.getGridRows().should("have.length", 7);
+ cy.getGridRow(0).should("contain", "1");
cy.getGridViewport().scrollTo("bottom");
cy.getGridRows().should("have.length", 7);
diff --git a/packages/browser-tests/cypress/integration/console/import.spec.js b/packages/browser-tests/cypress/integration/console/import.spec.js
index 51937abc9..c0148093e 100644
--- a/packages/browser-tests/cypress/integration/console/import.spec.js
+++ b/packages/browser-tests/cypress/integration/console/import.spec.js
@@ -6,8 +6,8 @@ describe("questdb import", () => {
});
it("display import panel", () => {
- cy.get('[data-hook="import-panel-button"]').click();
- cy.get('[data-hook="import-dropbox"]').should("be.visible");
+ cy.getByDataHook("import-panel-button").click();
+ cy.getByDataHook("import-dropbox").should("be.visible");
cy.matchImageSnapshot();
});
});
diff --git a/packages/browser-tests/cypress/integration/console/schema.spec.js b/packages/browser-tests/cypress/integration/console/schema.spec.js
new file mode 100644
index 000000000..b5cd8a393
--- /dev/null
+++ b/packages/browser-tests/cypress/integration/console/schema.spec.js
@@ -0,0 +1,145 @@
+///
+
+const baseUrl = "http://localhost:9999";
+
+const tables = [
+ "btc_trades",
+ "chicago_weather_stations",
+ "ecommerce_stats",
+ "gitlog",
+];
+
+describe("questdb schema with working tables", () => {
+ before(() => {
+ cy.visit(baseUrl);
+ tables.forEach((table) => {
+ cy.createTable(table);
+ });
+ });
+
+ it("should show all the tables when there are no suspended", () => {
+ tables.forEach((table) => {
+ cy.getByDataHook("schema-table-title").should("contain", table);
+ });
+ cy.getByDataHook("schema-filter-suspended-button").should("not.exist");
+ cy.getByDataHook("schema-suspension-popover-trigger").should("not.exist");
+ });
+
+ it("should filter the table with input field", () => {
+ // Table name search
+ cy.get('input[name="table_filter"]').type("btc_trades");
+ cy.getByDataHook("schema-search-clear-button").should("exist");
+ cy.getByDataHook("schema-table-title").should("contain", "btc_trades");
+ cy.getByDataHook("schema-table-title").should("not.contain", "ź");
+ cy.getByDataHook("schema-table-title").should(
+ "not.contain",
+ "ecommerce_stats"
+ );
+ cy.getByDataHook("schema-table-title").should("not.contain", "gitlog");
+ cy.getByDataHook("schema-search-clear-button").click();
+ cy.getByDataHook("schema-table-title").should(
+ "have.length.least",
+ tables.length
+ );
+
+ // Column name search
+ cy.get('input[name="table_filter"]').type("timestamp");
+ cy.getByDataHook("schema-table-title").should("contain", "btc_trades");
+ cy.getByDataHook("schema-table-title").should(
+ "not.contain",
+ "chicago_weather_stations"
+ );
+ cy.getByDataHook("schema-search-clear-button").click();
+ cy.getByDataHook("schema-table-title").should(
+ "contain",
+ "chicago_weather_stations"
+ );
+ });
+
+ after(() => {
+ tables.forEach((table) => {
+ cy.dropTable(table);
+ });
+ });
+});
+
+describe("questdb schema with suspended tables with Linux OS error codes", () => {
+ before(() => {
+ cy.visit(baseUrl);
+ cy.getEditorContent().should("be.visible");
+ cy.clearEditor();
+ tables.forEach((table) => {
+ cy.createTable(table);
+ });
+ cy.typeQuery(
+ "ALTER TABLE btc_trades SUSPEND WAL WITH 24, 'Too many open files';"
+ )
+ .clickRun()
+ .clearEditor();
+
+ cy.typeQuery(
+ "ALTER TABLE ecommerce_stats SUSPEND WAL WITH 12, 'Out of memory';"
+ )
+ .clickRun()
+ .clearEditor();
+ });
+ beforeEach(() => {
+ cy.visit(baseUrl);
+ cy.getEditorContent().should("be.visible");
+ cy.clearEditor();
+ });
+
+ it("should work with 2 suspended tables, btc_trades and ecommerce_stats", () => {
+ cy.getByDataHook("schema-filter-suspended-button").should(
+ "not.be.disabled"
+ );
+ cy.getByDataHook("schema-filter-suspended-button").should("contain", "2");
+ });
+
+ it("should filter out non-suspended tables on click", () => {
+ cy.getByDataHook("schema-filter-suspended-button").click();
+ cy.getByDataHook("schema-table-title").should("have.length", 2);
+ cy.getByDataHook("schema-table-title").should("contain", "btc_trades");
+ cy.getByDataHook("schema-table-title").should("contain", "ecommerce_stats");
+ cy.getByDataHook("schema-table-title").should("not.contain", "gitlog");
+ cy.getByDataHook("schema-table-title").should(
+ "not.contain",
+ "chicago_weather_stations"
+ );
+ cy.getByDataHook("schema-filter-suspended-button").click();
+ });
+
+ it("should show the suspension dialog on click with details for btc_trades", () => {
+ cy.get('input[name="table_filter"]').click().type("btc_trades");
+ cy.getByDataHook("schema-suspension-dialog-trigger").click();
+ cy.getByDataHook("schema-suspension-dialog").should(
+ "have.attr",
+ "data-table-name",
+ "btc_trades"
+ );
+ cy.getByDataHook("schema-suspension-dialog-error-message").should(
+ "contain",
+ "Too many open files, please, increase the maximum number of open file handlers OS limit"
+ );
+ cy.getByDataHook("schema-suspension-dialog-error-link").should(
+ "have.attr",
+ "href",
+ "https://questdb.io/docs/deployment/capacity-planning/#maximum-open-files"
+ );
+ });
+
+ it("should resume WAL for btc_trades from the suspension popover", () => {
+ cy.get('input[name="table_filter"]').click().type("btc_trades");
+ cy.contains("Suspended").click();
+ cy.getByDataHook("schema-suspension-dialog-restart-transaction").click();
+ cy.getByDataHook("schema-suspension-dialog-dismiss").click();
+ cy.getByDataHook("schema-suspension-dialog").should("not.exist");
+ cy.getByDataHook("schema-suspension-dialog-trigger").should("not.exist");
+ });
+
+ after(() => {
+ tables.forEach((table) => {
+ cy.dropTable(table);
+ });
+ });
+});
diff --git a/packages/browser-tests/cypress/integration/console/warnings.spec.js b/packages/browser-tests/cypress/integration/console/warnings.spec.js
new file mode 100644
index 000000000..b209d461c
--- /dev/null
+++ b/packages/browser-tests/cypress/integration/console/warnings.spec.js
@@ -0,0 +1,57 @@
+///
+
+const baseUrl = "http://localhost:9999";
+
+describe("System configuration - no warnings", () => {
+ before(() => {
+ cy.visit(baseUrl);
+ });
+
+ it("should not display warnings if there aren't any in /warnings", () => {
+ cy.getByDataHook("warnings").should("not.exist");
+ });
+});
+
+describe("System configuration - 3 warnings", () => {
+ after(() => {
+ cy.typeQuery("select simulate_warnings('', '');").runLine().clearEditor();
+ });
+
+ before(() => {
+ cy.visit(baseUrl);
+ cy.getEditorContent().should("be.visible");
+ cy.clearEditor();
+ [
+ "select simulate_warnings('UNSUPPORTED FILE SYSTEM', 'Unsupported file system [dir=/questdb/path/dbRoot, magic=0x6400A468]');",
+ "select simulate_warnings('TOO MANY OPEN FILES', 'fs.file-max limit is too low [current=1024, recommended=1048576]');",
+ "select simulate_warnings('OUT OF MMAP AREAS', 'vm.max_map_count limit is too low [current=4096, recommended=1048576]');",
+ ].forEach((query) => {
+ cy.typeQuery(query).runLine().clearEditor();
+ });
+ cy.visit(baseUrl);
+ });
+
+ it("should show all three warnings in the UI", () => {
+ cy.getByDataHook("warnings").should("be.visible");
+ cy.getByDataHook("warning").should("have.length", 3);
+ cy.getByDataHook("warning-text").should(
+ "contain",
+ "Unsupported file system"
+ );
+ cy.getByDataHook("warning-text").should(
+ "contain",
+ "fs.file-max limit is too low"
+ );
+ cy.getByDataHook("warning-text").should(
+ "contain",
+ "vm.max_map_count limit is too low"
+ );
+ });
+
+ it("should clear out all the warnings", () => {
+ [0, 1, 2].forEach((idx) => {
+ cy.getByDataHook("warning-close-button").eq(0).click();
+ });
+ cy.getByDataHook("warnings").should("not.exist");
+ });
+});
diff --git a/packages/browser-tests/questdb b/packages/browser-tests/questdb
index 05c8dbc31..22d99f070 160000
--- a/packages/browser-tests/questdb
+++ b/packages/browser-tests/questdb
@@ -1 +1 @@
-Subproject commit 05c8dbc31a45318e72dda1b15f9fceb8f85f82ea
+Subproject commit 22d99f0703d6b8c64e1e1229fc2ec7e2dcdd0cea
diff --git a/packages/web-console/assets/icon-copy.svg b/packages/web-console/assets/icon-copy.svg
new file mode 100644
index 000000000..09d93ba15
--- /dev/null
+++ b/packages/web-console/assets/icon-copy.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/web-console/assets/icon-database.svg b/packages/web-console/assets/icon-database.svg
new file mode 100644
index 000000000..1509081b2
--- /dev/null
+++ b/packages/web-console/assets/icon-database.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/web-console/assets/line.svg b/packages/web-console/assets/line.svg
new file mode 100644
index 000000000..49316f5e4
--- /dev/null
+++ b/packages/web-console/assets/line.svg
@@ -0,0 +1,4 @@
+
diff --git a/packages/web-console/package.json b/packages/web-console/package.json
index aac6abaa9..8553eddd5 100644
--- a/packages/web-console/package.json
+++ b/packages/web-console/package.json
@@ -66,6 +66,7 @@
"react": "17.0.2",
"react-contextmenu": "2.14.0",
"react-dom": "17.0.2",
+ "react-highlight-words": "^0.20.0",
"react-hook-form": "7.22.3",
"react-is": "^18.1.0",
"react-markdown": "^8.0.7",
@@ -97,6 +98,7 @@
"@types/ramda": "0.27.40",
"@types/react": "16.9.35",
"@types/react-dom": "16.9.8",
+ "@types/react-highlight-words": "^0.16.7",
"@types/react-redux": "7.1.9",
"@types/react-transition-group": "4.4.0",
"@types/styled-components": "5.1.7",
diff --git a/packages/web-console/serve-dist.js b/packages/web-console/serve-dist.js
index 87f5e00f0..88ea82001 100644
--- a/packages/web-console/serve-dist.js
+++ b/packages/web-console/serve-dist.js
@@ -9,7 +9,8 @@ const server = http.createServer((req, res) => {
if (
urlData.pathname.startsWith("/exec") ||
- urlData.pathname.startsWith("/settings")
+ urlData.pathname.startsWith("/settings") ||
+ urlData.pathname.startsWith("/warnings")
) {
// proxy /exec requests to localhost:9000
const options = {
diff --git a/packages/web-console/src/components/Form/FormSubmit/index.tsx b/packages/web-console/src/components/Form/FormSubmit/index.tsx
index 19ae24031..be802f96e 100644
--- a/packages/web-console/src/components/Form/FormSubmit/index.tsx
+++ b/packages/web-console/src/components/Form/FormSubmit/index.tsx
@@ -5,7 +5,7 @@ import { Button } from "@questdb/react-components"
type Props = {
children: React.ReactNode
disabled?: boolean
- variant?: "error" | "success" | "warning" | "primary"
+ variant?: "error" | "success" | "warning" | "primary" | "secondary"
prefixIcon?: ButtonProps["prefixIcon"]
}
@@ -14,6 +14,7 @@ export const FormSubmit = ({
disabled,
variant,
prefixIcon,
+ ...rest
}: Props) => (
diff --git a/packages/web-console/src/components/IconWithTooltip/index.tsx b/packages/web-console/src/components/IconWithTooltip/index.tsx
index 07aeb41af..d59b1d654 100644
--- a/packages/web-console/src/components/IconWithTooltip/index.tsx
+++ b/packages/web-console/src/components/IconWithTooltip/index.tsx
@@ -3,7 +3,7 @@ import { PopperHover, Tooltip, Placement } from "../"
type Props = {
icon: ReactNode
- tooltip: string
+ tooltip: ReactNode
placement: Placement
}
diff --git a/packages/web-console/src/components/TableSchemaDialog/dialog.tsx b/packages/web-console/src/components/TableSchemaDialog/dialog.tsx
index f61fb51eb..163eea076 100644
--- a/packages/web-console/src/components/TableSchemaDialog/dialog.tsx
+++ b/packages/web-console/src/components/TableSchemaDialog/dialog.tsx
@@ -228,7 +228,7 @@ export const Dialog = ({
{action === "add" && (
-
+
)}
diff --git a/packages/web-console/src/providers/SettingsProvider/index.tsx b/packages/web-console/src/providers/SettingsProvider/index.tsx
index 036687414..44ae51869 100644
--- a/packages/web-console/src/providers/SettingsProvider/index.tsx
+++ b/packages/web-console/src/providers/SettingsProvider/index.tsx
@@ -5,7 +5,7 @@ import React, {
useReducer,
useState,
} from "react"
-import { ConsoleConfig, Settings } from "./types"
+import { ConsoleConfig, Settings, Warning } from "./types"
import { CenteredLayout } from "../../components"
import { Box, Button, Text } from "@questdb/react-components"
import { Refresh } from "@styled-icons/remix-line"
@@ -25,9 +25,10 @@ const reducer = (s: State, n: Partial) => ({ ...s, ...n })
const SettingContext = createContext<{
settings: Settings
consoleConfig: ConsoleConfig
-}>({ settings: {}, consoleConfig: {} })
+ warnings: Warning[]
+}>({ settings: {}, consoleConfig: {}, warnings: [] })
-const settingsError = (
+const connectionError = (
<>
Error connecting to the database.
@@ -44,12 +45,13 @@ export const SettingsProvider = ({
}) => {
const [state, dispatch] = useReducer(reducer, initialState)
const [settings, setSettings] = useState({})
+ const [warnings, setWarnings] = useState([])
const [consoleConfig, setConsoleConfig] = useState({})
const views: { [key in View]: () => React.ReactNode } = {
[View.loading]: () => null,
[View.ready]: () => (
-
+
{children}
),
@@ -81,40 +83,40 @@ export const SettingsProvider = ({
),
}
- const fetchSettings = async () => {
+ const fetchEndpoint = async (
+ endpoint: string,
+ errorMessage: React.ReactNode,
+ ) => {
try {
- const response = await fetch(`settings`)
+ const response = await fetch(endpoint)
if (response.status === 200) {
- const settings = await response.json()
- setSettings(settings)
+ return await response.json()
} else {
- dispatch({ view: View.error, errorMessage: settingsError })
+ dispatch({ view: View.error, errorMessage })
}
} catch (e) {
- dispatch({ view: View.error, errorMessage: settingsError })
+ dispatch({ view: View.error, errorMessage })
}
}
- const fetchConsoleConfig = async () => {
- try {
- const response = await fetch(
- `assets/console-configuration.json`
+ useEffect(() => {
+ const fetchAll = async () => {
+ const settings = await fetchEndpoint("settings", connectionError)
+ const warnings = await fetchEndpoint("warnings", connectionError)
+ const consoleConfig = await fetchEndpoint(
+ "assets/console-configuration.json",
+ consoleConfigError,
)
- if (response.status === 200) {
- const consoleConfig = await response.json()
+ if (settings) {
+ setSettings(settings)
+ }
+ if (warnings) {
+ setWarnings(warnings)
+ // setWarnings(warningMock)
+ }
+ if (consoleConfig) {
setConsoleConfig(consoleConfig)
- } else {
- dispatch({ view: View.error, errorMessage: consoleConfigError })
}
- } catch (e) {
- dispatch({ view: View.error, errorMessage: consoleConfigError })
- }
- }
-
- useEffect(() => {
- const fetchAll = async () => {
- await fetchSettings()
- await fetchConsoleConfig()
}
fetchAll().then(() => dispatch({ view: View.ready }))
diff --git a/packages/web-console/src/providers/SettingsProvider/types.ts b/packages/web-console/src/providers/SettingsProvider/types.ts
index a1229b716..ab9161a01 100644
--- a/packages/web-console/src/providers/SettingsProvider/types.ts
+++ b/packages/web-console/src/providers/SettingsProvider/types.ts
@@ -1,3 +1,4 @@
+import { ErrorTag } from "../../utils"
export type Settings = Partial<{
"release.type": "OSS" | "EE"
"release.version": string
@@ -31,3 +32,8 @@ export type ConsoleConfig = Partial<{
readOnly?: boolean
savedQueries: Array
}>
+
+export type Warning = {
+ tag: ErrorTag
+ warning: string
+}
diff --git a/packages/web-console/src/scenes/Editor/Menu/index.tsx b/packages/web-console/src/scenes/Editor/Menu/index.tsx
index 921a104ff..cddfcb7b2 100644
--- a/packages/web-console/src/scenes/Editor/Menu/index.tsx
+++ b/packages/web-console/src/scenes/Editor/Menu/index.tsx
@@ -28,6 +28,7 @@ import { CSSTransition } from "react-transition-group"
import styled from "styled-components"
import { Add, Close as _CloseIcon, Play, Stop } from "@styled-icons/remix-line"
import { Menu as _MenuIcon } from "@styled-icons/remix-fill"
+import { CornerDownLeft } from "@styled-icons/evaicons-solid"
import {
ErrorButton,
@@ -40,8 +41,9 @@ import {
useKeyPress,
useScreenSize,
} from "../../../components"
+import { Box } from "@questdb/react-components"
import { actions, selectors } from "../../../store"
-import { color } from "../../../utils"
+import { color, platform } from "../../../utils"
import QueryPicker from "../QueryPicker"
import { useLocalStorage } from "../../../providers/LocalStorageProvider"
import { StoreKey } from "../../../utils/localStorage/types"
@@ -117,12 +119,30 @@ const SideMenuMenuButton = styled(TransparentButton)`
}
`
+const Key = styled(Box).attrs({ alignItems: "center" })`
+ padding: 0 4px;
+ background: #61647a;
+ border-radius: 2px;
+ font-size: 1.2rem;
+ height: 1.8rem;
+
+ &:not(:last-child) {
+ margin-right: 0.25rem;
+ }
+`
+
+const RunShortcut = styled(Box).attrs({ alignItems: "center", gap: "0" })`
+ margin-left: 1.5rem;
+`
+
const MenuItems = styled.div`
display: grid;
grid-auto-flow: column;
align-items: center;
`
+const ctrlCmd = platform.isMacintosh || platform.isIOS ? "⌘" : "Ctrl"
+
const Menu = () => {
const dispatch = useDispatch()
const [queriesPopperActive, setQueriesPopperActive] = useState()
@@ -193,6 +213,12 @@ const Menu = () => {
Run
+
+ {ctrlCmd}
+
+
+
+
)}
diff --git a/packages/web-console/src/scenes/Editor/Monaco/index.tsx b/packages/web-console/src/scenes/Editor/Monaco/index.tsx
index 99765ccd2..dd3418649 100644
--- a/packages/web-console/src/scenes/Editor/Monaco/index.tsx
+++ b/packages/web-console/src/scenes/Editor/Monaco/index.tsx
@@ -32,7 +32,6 @@ import { createSchemaCompletionProvider } from "./questdb-sql"
import { color } from "../../../utils"
import { eventBus } from "../../../modules/EventBus"
import { EventType } from "../../../modules/EventBus/types"
-import { InformationSchemaColumn } from "./questdb-sql/types"
loader.config({
paths: {
@@ -104,6 +103,7 @@ const MonacoEditor = () => {
const dispatch = useDispatch()
const running = useSelector(selectors.query.getRunning)
const tables = useSelector(selectors.query.getTables)
+ const columns = useSelector(selectors.query.getColumns)
const [schemaCompletionHandle, setSchemaCompletionHandle] =
useState()
const decorationsRef = useRef()
@@ -377,32 +377,13 @@ const MonacoEditor = () => {
if (editorReady && monacoRef?.current && editorRef?.current) {
schemaCompletionHandle?.dispose()
setRefreshingTables(true)
- try {
- const response = await quest.query(
- "information_schema.columns()",
- )
- if (response.type === QuestDB.Type.DQL) {
- setSchemaCompletionHandle(
- monacoRef.current.languages.registerCompletionItemProvider(
- QuestDBLanguageName,
- createSchemaCompletionProvider(
- editorRef.current,
- tables,
- response.data,
- ),
- ),
- )
- }
- } catch (e) {
- setSchemaCompletionHandle(
- monacoRef.current.languages.registerCompletionItemProvider(
- QuestDBLanguageName,
- createSchemaCompletionProvider(editorRef.current, tables),
- ),
- )
- } finally {
- setRefreshingTables(false)
- }
+ setSchemaCompletionHandle(
+ monacoRef.current.languages.registerCompletionItemProvider(
+ QuestDBLanguageName,
+ createSchemaCompletionProvider(editorRef.current, tables, columns),
+ ),
+ )
+ setRefreshingTables(false)
}
}
@@ -410,7 +391,7 @@ const MonacoEditor = () => {
if (!refreshingTables) {
setCompletionProvider()
}
- }, [tables, monacoRef, editorReady])
+ }, [tables, columns, monacoRef, editorReady])
useEffect(() => {
window.addEventListener("focus", setCompletionProvider)
diff --git a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/createSchemaCompletionProvider.ts b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/createSchemaCompletionProvider.ts
index 0de5ec4dc..520748d15 100644
--- a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/createSchemaCompletionProvider.ts
+++ b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/createSchemaCompletionProvider.ts
@@ -1,6 +1,6 @@
-import { Table, uniq } from "../../../../utils"
+import { Table, uniq, InformationSchemaColumn } from "../../../../utils"
import * as monaco from "monaco-editor"
-import { CompletionItemPriority, InformationSchemaColumn } from "./types"
+import { CompletionItemPriority } from "./types"
import { findMatches, getQueryFromCursor } from "../utils"
import { getTableCompletions } from "./getTableCompletions"
import { getColumnCompletions } from "./getColumnCompletions"
diff --git a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/getColumnCompletions.ts b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/getColumnCompletions.ts
index 84b18d89e..2cb39d50f 100644
--- a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/getColumnCompletions.ts
+++ b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/getColumnCompletions.ts
@@ -1,9 +1,5 @@
-import { uniq } from "../../../../utils"
-import {
- CompletionItemKind,
- CompletionItemPriority,
- InformationSchemaColumn,
-} from "./types"
+import { uniq, InformationSchemaColumn } from "../../../../utils"
+import { CompletionItemKind, CompletionItemPriority } from "./types"
import type { IRange } from "monaco-editor"
export const getColumnCompletions = ({
diff --git a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/types.ts b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/types.ts
index b5bbad50d..c05c2a328 100644
--- a/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/types.ts
+++ b/packages/web-console/src/scenes/Editor/Monaco/questdb-sql/types.ts
@@ -1,10 +1,3 @@
-export type InformationSchemaColumn = {
- table_name: string
- ordinal_position: number
- column_name: string
- data_type: string
-}
-
export enum CompletionItemPriority {
High = "1",
MediumHigh = "2",
diff --git a/packages/web-console/src/scenes/Editor/Monaco/utils.ts b/packages/web-console/src/scenes/Editor/Monaco/utils.ts
index 7b0c03e71..ea2c7546b 100644
--- a/packages/web-console/src/scenes/Editor/Monaco/utils.ts
+++ b/packages/web-console/src/scenes/Editor/Monaco/utils.ts
@@ -443,7 +443,6 @@ export const appendQuery = (
if (model) {
const position = editor.getPosition()
- const lineCount = model.getLineCount()
if (position) {
const newQueryLines = query.split("\n")
@@ -491,7 +490,7 @@ export const appendQuery = (
editor.focus()
if (options.appendAt === "end") {
- editor.revealLine(lineCount)
+ editor.revealLine(model.getLineCount())
}
}
}
diff --git a/packages/web-console/src/scenes/Layout/help.tsx b/packages/web-console/src/scenes/Layout/help.tsx
index e2d6d015a..8927394b2 100644
--- a/packages/web-console/src/scenes/Layout/help.tsx
+++ b/packages/web-console/src/scenes/Layout/help.tsx
@@ -9,8 +9,8 @@ import {
PopperToggle,
} from "../../components"
import { DropdownMenu, FeedbackDialog, Box } from "@questdb/react-components"
-import { BUTTON_ICON_SIZE } from "../../consts/index"
-import { IconWithTooltip } from "../../components/IconWithTooltip"
+import { BUTTON_ICON_SIZE } from "../../consts"
+import { IconWithTooltip } from "../../components"
import { useState, useCallback, useContext } from "react"
import { QuestContext } from "../../providers"
import { useSelector } from "react-redux"
diff --git a/packages/web-console/src/scenes/Layout/index.tsx b/packages/web-console/src/scenes/Layout/index.tsx
index 0659360e6..007eb2095 100644
--- a/packages/web-console/src/scenes/Layout/index.tsx
+++ b/packages/web-console/src/scenes/Layout/index.tsx
@@ -35,6 +35,7 @@ import News from "../../scenes/News"
import { CreateTableDialog } from "../../components/CreateTableDialog"
import { EditorProvider } from "../../providers"
import { Help } from "./help"
+import { Warnings } from "./warning"
import "allotment/dist/style.css"
@@ -77,6 +78,7 @@ const Layout = () => {
return (
+
diff --git a/packages/web-console/src/scenes/Layout/warning.tsx b/packages/web-console/src/scenes/Layout/warning.tsx
new file mode 100644
index 000000000..ac21ee0ed
--- /dev/null
+++ b/packages/web-console/src/scenes/Layout/warning.tsx
@@ -0,0 +1,106 @@
+import React, { useEffect, useState } from "react"
+import styled from "styled-components"
+import { Box } from "@questdb/react-components"
+import { useSettings } from "../../providers"
+import { Close, ErrorWarning, ExternalLink } from "@styled-icons/remix-line"
+import { errorWorkarounds } from "../../utils/errorWorkarounds"
+import { ErrorTag } from "utils"
+
+const WarningRoot = styled(Box).attrs({
+ align: "center",
+ justifyContent: "space-between",
+})`
+ width: 100%;
+ height: 4rem;
+ background: #f8a24d;
+ color: #000;
+`
+
+const Content = styled(Box).attrs({ gap: "0.5rem" })`
+ padding: 0 1.5rem;
+`
+
+const CloseButton = styled(Box).attrs({
+ align: "center",
+ justifyContent: "center",
+})`
+ border: 0;
+ cursor: pointer;
+ width: 4.5rem;
+ height: 4.5rem;
+`
+
+const WarningText = styled.span`
+ font-weight: 600;
+`
+
+const WorkaroundLink = styled.a`
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ margin-left: 0.5rem;
+ color: #000;
+
+ &:hover {
+ text-decoration: none;
+ }
+`
+
+const CloseIcon = styled(Close)`
+ color: #000;
+`
+
+export const Warnings = () => {
+ const { warnings } = useSettings()
+ const [open, setOpen] = useState([])
+
+ useEffect(() => {
+ if (warnings && warnings.length > 0) {
+ setOpen(warnings.map((warning) => warning.tag))
+ }
+ }, [warnings])
+
+ if (open.length === 0) return null
+
+ return (
+
+ {warnings
+ .filter((warning) => open.includes(warning.tag))
+ .map((warning, index) => (
+
+
+
+ Warning:{" "}
+
+ {warning.warning}
+
+ {errorWorkarounds[warning.tag] && (
+
+
+ {errorWorkarounds[warning.tag].title}
+
+ )}
+
+
+ setOpen(open.filter((errorTag) => errorTag !== warning.tag))
+ }
+ >
+
+
+
+ ))}
+
+ )
+}
diff --git a/packages/web-console/src/scenes/News/index.tsx b/packages/web-console/src/scenes/News/index.tsx
index 74abdab26..abe9133ac 100644
--- a/packages/web-console/src/scenes/News/index.tsx
+++ b/packages/web-console/src/scenes/News/index.tsx
@@ -7,7 +7,7 @@ import {
import styled from "styled-components"
import React, { useEffect, useState, useContext } from "react"
import { QuestContext } from "../../providers"
-import { NewsItem } from "../../utils/questdb"
+import { NewsItem } from "../../utils"
import { useDispatch, useSelector } from "react-redux"
import { selectors, actions } from "../../store"
import ReactMarkdown from "react-markdown"
@@ -16,7 +16,7 @@ import { db } from "../../store/db"
import { UnreadItemsIcon } from "../../components/UnreadItemsIcon"
import { Thumbnail } from "./thumbnail"
import { Bell } from "./bell"
-import { BUTTON_ICON_SIZE } from "../../consts/index"
+import { BUTTON_ICON_SIZE } from "../../consts"
const Loading = styled.div`
display: grid;
diff --git a/packages/web-console/src/scenes/Schema/Row/index.tsx b/packages/web-console/src/scenes/Schema/Row/index.tsx
index b5728bc5d..0acdc5b14 100644
--- a/packages/web-console/src/scenes/Schema/Row/index.tsx
+++ b/packages/web-console/src/scenes/Schema/Row/index.tsx
@@ -22,27 +22,23 @@
*
******************************************************************************/
-import React, { MouseEvent, ReactNode, useCallback } from "react"
+import React, { MouseEvent, ReactNode, useContext } from "react"
import styled from "styled-components"
import { Rocket } from "@styled-icons/boxicons-regular"
import { SortDown } from "@styled-icons/boxicons-regular"
import { RightArrow } from "@styled-icons/boxicons-regular"
import { CheckboxBlankCircle } from "@styled-icons/remix-line"
-import { CodeSSlash } from "@styled-icons/remix-line"
import { Information } from "@styled-icons/remix-line"
-import { Table as TableIcon } from "@styled-icons/remix-line"
-import { FileList, PieChart } from "@styled-icons/remix-line"
import type { TreeNodeKind } from "../../../components/Tree"
+import * as QuestDB from "../../../utils/questdb"
+import Highlighter from "react-highlight-words"
+import { TableIcon } from "../table-icon"
-import {
- SecondaryButton,
- Text,
- TransitionDuration,
- IconWithTooltip,
-} from "../../../components"
+import { Text, TransitionDuration, IconWithTooltip } from "../../../components"
import type { TextProps } from "../../../components"
import { color } from "../../../utils"
-import { useEditor } from "../../../providers"
+import { SchemaContext } from "../SchemaContext"
+import { SuspensionDialog } from "../SuspensionDialog"
type Props = Readonly<{
className?: string
@@ -53,8 +49,9 @@ type Props = Readonly<{
kind: TreeNodeKind
name: string
onClick?: (event: MouseEvent) => void
- partitionBy?: string
+ partitionBy?: QuestDB.PartitionBy
walEnabled?: boolean
+ walTableData?: QuestDB.WalTable
suffix?: ReactNode
tooltip?: boolean
type?: string
@@ -69,38 +66,39 @@ const Type = styled(Text)`
const Title = styled(Text)`
cursor: ${({ kind }) =>
["folder", "table"].includes(kind) ? "pointer" : "initial"};
-`
-const PlusButton = styled(SecondaryButton)>`
- position: absolute;
- right: ${({ tooltip }) => (tooltip ? "3rem" : "1rem")};
- margin-left: 1rem;
- opacity: 0;
+ .highlight {
+ background-color: #7c804f;
+ color: ${({ theme }) => theme.color.foreground};
+ }
`
-const Wrapper = styled.div>`
+const Wrapper = styled.div & { suspended?: boolean }>`
position: relative;
display: flex;
flex-direction: column;
- padding: 0.5rem 0;
+ padding: ${({ suspended }) => (suspended ? "0" : "0.5rem 0")};
padding-left: 1rem;
transition: background ${TransitionDuration.REG}ms;
- &:hover
- ${/* sc-selector */ PlusButton},
- &:active
- ${/* sc-selector */ PlusButton} {
- opacity: 1;
- }
-
&:hover,
&:active {
background: ${color("selection")};
}
+`
- &:hover ${/* sc-selector */ Type} {
- opacity: 0;
- }
+const HitBox = styled.div`
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ left: 0;
+ top: 0;
+`
+
+const TableActions = styled.span`
+ z-index: 1;
+ position: relative;
+ margin-right: 1rem;
`
const FlexRow = styled.div`
@@ -141,13 +139,6 @@ const DotIcon = styled(CheckboxBlankCircle)`
margin-right: 1rem;
`
-const TitleIcon = styled(TableIcon)`
- min-height: 18px;
- min-width: 18px;
- margin-right: 1rem;
- color: ${color("cyan")};
-`
-
const InfoIconWrapper = styled.div`
display: flex;
padding: 0 1rem;
@@ -155,22 +146,6 @@ const InfoIconWrapper = styled.div`
justify-content: center;
`
-const PartitionByWrapper = styled.div`
- margin-right: 1rem;
- display: flex;
- align-items: center;
-`
-
-const PieChartIcon = styled(PieChart)`
- color: ${color("gray2")};
- margin-right: 0.5rem;
-`
-
-const FileListIcon = styled(FileList)`
- color: ${color("yellow")};
- margin-right: 0.5rem;
-`
-
const Row = ({
className,
designatedTimestamp,
@@ -181,27 +156,29 @@ const Row = ({
name,
partitionBy,
walEnabled,
+ walTableData,
onClick,
suffix,
tooltip,
type,
}: Props) => {
- const { insertTextAtCursor } = useEditor()
-
- const handlePlusButtonClick = useCallback(
- (event: MouseEvent) => {
- event.stopPropagation()
- insertTextAtCursor(
- kind === "table" && !/^[a-z0-9_]+$/i.test(name) ? `"${name}"` : name,
- )
- },
- [name, kind],
- )
+ const { query } = useContext(SchemaContext)
return (
-
+
+
- {kind === "table" && }
+ {kind === "table" && (
+
+ )}
{kind === "column" && indexed && (
)}
-
- {name}
+
+
{suffix}
@@ -240,29 +226,10 @@ const Row = ({
)}
- {kind === "table" && partitionBy !== "NONE" && (
-
-
- {partitionBy}
-
- )}
-
- {kind === "table" && walEnabled && (
-
-
- WAL
-
- )}
-
- {["column", "table"].includes(kind) && (
-
-
- Add
-
+ {walTableData?.suspended && kind === "table" && (
+
+
+
)}
{tooltip && description && (
diff --git a/packages/web-console/src/scenes/Schema/SchemaContext.tsx b/packages/web-console/src/scenes/Schema/SchemaContext.tsx
new file mode 100644
index 000000000..819e925f9
--- /dev/null
+++ b/packages/web-console/src/scenes/Schema/SchemaContext.tsx
@@ -0,0 +1,9 @@
+import { createContext } from "react"
+
+export const SchemaContext = createContext<{
+ query: string
+ setQuery: (query: string) => void
+}>({
+ query: "",
+ setQuery: () => {},
+})
diff --git a/packages/web-console/src/scenes/Schema/SuspensionDialog/index.tsx b/packages/web-console/src/scenes/Schema/SuspensionDialog/index.tsx
new file mode 100644
index 000000000..fb9e0d1cd
--- /dev/null
+++ b/packages/web-console/src/scenes/Schema/SuspensionDialog/index.tsx
@@ -0,0 +1,319 @@
+import React from "react"
+import {
+ Dialog,
+ ForwardRef,
+ Button,
+ Overlay,
+ Input,
+} from "@questdb/react-components"
+import { Error as ErrorIcon, Undo } from "@styled-icons/boxicons-regular"
+import styled from "styled-components"
+import * as QuestDB from "../../../utils/questdb"
+import {
+ FileCopy,
+ ExternalLink,
+ Restart,
+ Table,
+} from "@styled-icons/remix-line"
+import { Box } from "../../../components/Box"
+import { Form } from "../../../components/Form"
+import { useState, useContext, useEffect } from "react"
+import { QuestContext } from "../../../providers"
+import { eventBus } from "../../../modules/EventBus"
+import { EventType } from "../../../modules/EventBus/types"
+import { ErrorResult } from "../../../utils"
+import { Text, Link } from "../../../components"
+import { errorWorkarounds } from "../../../utils/errorWorkarounds"
+import Joi from "joi"
+
+const ErrorButton = styled(Button)`
+ background: #3e1b1b;
+ border: 1px #723131 solid;
+ color: #f47474;
+ padding: 3px 10px;
+ font-size: 1.3rem;
+`
+
+const StyledDialogContent = styled(Dialog.Content)`
+ border-color: #723131;
+`
+
+const StyledDescription = styled(Dialog.Description)`
+ display: grid;
+ gap: 2rem;
+`
+
+const ContentBlockBox = styled(Box).attrs({
+ align: "center",
+ flexDirection: "column",
+})`
+ width: 100%;
+`
+
+const FormWrapper = styled.div`
+ width: 100%;
+ display: grid;
+ grid-template-columns: 100px auto;
+ gap: 1rem;
+`
+
+const TransactionInput = styled(Form.Input)`
+ width: 10rem;
+`
+
+const Icon = styled(Box)`
+ height: 4.8rem;
+`
+
+const StyledInput = styled(Input)`
+ width: 100%;
+ font-family: ${({ theme }) => theme.fontMonospace};
+ background: #313340;
+ border-color: ${({ theme }) => theme.color.selection};
+`
+
+type FormValues = {
+ resume_transaction_id?: number
+}
+
+const GENERIC_ERROR_TEXT = "Error restarting transaction"
+
+export const SuspensionDialog = ({
+ walTableData,
+}: {
+ walTableData: QuestDB.WalTable
+}) => {
+ const [active, setActive] = useState(false)
+ const { quest } = useContext(QuestContext)
+ const [isSubmitting, setIsSubmitting] = useState(false)
+ const [isSubmitted, setIsSubmitted] = useState(false)
+ const [error, setError] = useState()
+
+ const handleSubmit = async (values: FormValues) => {
+ setIsSubmitting(true)
+ setError(undefined)
+ try {
+ const response = await quest.query(
+ `ALTER TABLE ${walTableData.name} RESUME WAL${
+ values.resume_transaction_id
+ ? ` FROM TRANSACTION ${values.resume_transaction_id}`
+ : ""
+ }`,
+ )
+ if (response && response.type === QuestDB.Type.DDL) {
+ setIsSubmitted(true)
+ } else {
+ setError(GENERIC_ERROR_TEXT)
+ }
+ setIsSubmitting(false)
+ } catch (e) {
+ const error = e as ErrorResult
+ setIsSubmitting(false)
+ setError(`${GENERIC_ERROR_TEXT}${error.error ? `: ${error.error}` : ""}`)
+ }
+ }
+
+ useEffect(() => {
+ if (active) {
+ setError(undefined)
+ setIsSubmitted(false)
+ }
+ }, [active])
+
+ return (
+ {
+ if (!open) {
+ eventBus.publish(EventType.MSG_QUERY_SCHEMA)
+ }
+ }}
+ >
+
+
+ }
+ data-hook="schema-suspension-dialog-trigger"
+ >
+ Suspended
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Table is suspended: {walTableData.name}
+
+
+
+
+
+ {error && {error}}
+
+ {isSubmitted && (
+ WAL resumed successfully!
+ )}
+
+ {walTableData.errorTag &&
+ errorWorkarounds[walTableData.errorTag] && (
+
+ {errorWorkarounds[walTableData.errorTag].message}
+
+ )}
+
+
+
+
+
+
+
+ {walTableData.sequencerTxn}
+
+
+
+
+
+
+
+
+
+
+ {walTableData.writerTxn}
+
+
+
+
+
+ {walTableData.writerLagTxnCount} transaction
+ {walTableData.writerLagTxnCount === "1" ? "" : "s"} behind
+
+
+ {walTableData.errorMessage && (
+
+ Server message:
+
+
+
+
+
+ )}
+
+ {walTableData.errorTag &&
+ errorWorkarounds[walTableData.errorTag] && (
+
+
+ Workarounds and documentation:
+
+
+
+
+ {errorWorkarounds[walTableData.errorTag].title}
+
+
+
+ )}
+
+
+
+ If you have addressed the issue, restart the process:
+
+
+
+
+
+
+
+
+ }
+ skin="secondary"
+ data-hook="schema-suspension-dialog-dismiss"
+ >
+ Dismiss
+
+
+
+
+
+
+ )
+}
diff --git a/packages/web-console/src/scenes/Schema/Table/index.tsx b/packages/web-console/src/scenes/Schema/Table/index.tsx
index 3a6642844..8f6e0bdf5 100644
--- a/packages/web-console/src/scenes/Schema/Table/index.tsx
+++ b/packages/web-console/src/scenes/Schema/Table/index.tsx
@@ -45,6 +45,7 @@ type Props = QuestDB.Table &
partitionBy: string
expanded?: boolean
onChange: (name: string) => void
+ walTableData?: QuestDB.WalTable
}>
const Wrapper = styled.div`
@@ -54,6 +55,7 @@ const Wrapper = styled.div`
align-items: stretch;
flex-direction: column;
overflow: hidden;
+ font-family: ${({ theme }) => theme.fontMonospace};
${collapseTransition};
`
@@ -118,6 +120,7 @@ const Table = ({
partitionBy,
expanded = false,
walEnabled,
+ walTableData,
onChange,
dedup,
}: Props) => {
@@ -192,6 +195,7 @@ const Table = ({
}}
partitionBy={partitionBy}
walEnabled={walEnabled}
+ walTableData={walTableData}
suffix={isLoading && }
tooltip={!!description}
/>
diff --git a/packages/web-console/src/scenes/Schema/Toolbar/toolbar.tsx b/packages/web-console/src/scenes/Schema/Toolbar/toolbar.tsx
new file mode 100644
index 000000000..25e4590bd
--- /dev/null
+++ b/packages/web-console/src/scenes/Schema/Toolbar/toolbar.tsx
@@ -0,0 +1,122 @@
+import React, { useContext } from "react"
+import styled from "styled-components"
+import { Button, Box, Input } from "@questdb/react-components"
+import { Close, Filter3 } from "@styled-icons/remix-line"
+import { Error as ErrorIcon } from "@styled-icons/boxicons-regular"
+import { PopperHover, Tooltip } from "../../../components"
+import { SchemaContext } from "../SchemaContext"
+
+const Root = styled(Box).attrs({
+ justifyContent: "space-between",
+ gap: "1rem",
+ alignItems: "center",
+})`
+ width: 100%;
+ padding: 0 2rem;
+ margin-bottom: 1rem;
+`
+
+const Filter = styled.div`
+ position: relative;
+ display: flex;
+ width: 100%;
+`
+
+const FilterIcon = styled(Filter3)`
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ left: 1rem;
+ color: ${({ theme }) => theme.color.gray2};
+`
+
+const CloseIcon = styled(Close)`
+ position: absolute;
+ top: 50%;
+ transform: translateY(-50%);
+ right: 0.5rem;
+`
+
+const StyledInput = styled(Input)`
+ padding-left: 3.5rem;
+ padding-right: 3.5rem;
+ width: 100%;
+
+ &::placeholder {
+ color: ${({ theme }) => theme.color.foreground};
+ }
+`
+
+const StyledButton = styled(Button)`
+ &:disabled {
+ border: none;
+ }
+`
+
+const Error = styled(Box).attrs({ gap: "0.5rem" })<{
+ suspendedTablesCount: number
+}>`
+ &,
+ button {
+ color: ${({ theme, suspendedTablesCount }) =>
+ theme.color[`${suspendedTablesCount > 0 ? "red" : "gray1"}`]};
+ cursor: ${({ suspendedTablesCount }) =>
+ suspendedTablesCount > 0 ? "pointer" : "default"};
+ }
+`
+
+export const Toolbar = ({
+ suspendedTablesCount,
+ filterSuspendedOnly,
+ setFilterSuspendedOnly,
+}: {
+ suspendedTablesCount: number
+ filterSuspendedOnly: boolean
+ setFilterSuspendedOnly: (filter: boolean) => void
+}) => {
+ const { setQuery } = useContext(SchemaContext)
+ const queryRef = React.useRef(null)
+
+ return (
+
+
+
+ {queryRef.current?.value && (
+ {
+ setQuery("")
+ queryRef.current?.value && (queryRef.current.value = "")
+ }}
+ data-hook="schema-search-clear-button"
+ />
+ )}
+ setQuery(e.target.value)}
+ />
+
+ {suspendedTablesCount > 0 && (
+
+ setFilterSuspendedOnly(!filterSuspendedOnly)}
+ prefixIcon={}
+ data-hook="schema-filter-suspended-button"
+ >
+ {suspendedTablesCount}
+
+ }
+ >
+ Show suspended tables
+
+
+ )}
+
+ )
+}
diff --git a/packages/web-console/src/scenes/Schema/index.tsx b/packages/web-console/src/scenes/Schema/index.tsx
index ee074766b..8e7b4ad02 100644
--- a/packages/web-console/src/scenes/Schema/index.tsx
+++ b/packages/web-console/src/scenes/Schema/index.tsx
@@ -30,12 +30,17 @@ import React, {
useEffect,
useState,
useContext,
+ useReducer,
} from "react"
import { useDispatch } from "react-redux"
-import { from, combineLatest, of } from "rxjs"
-import { delay, startWith } from "rxjs/operators"
import styled, { css } from "styled-components"
-import { FileCopy, Loader3, Refresh, Search } from "@styled-icons/remix-line"
+import {
+ Add,
+ FileCopy,
+ Loader3,
+ Refresh,
+ Settings4,
+} from "@styled-icons/remix-line"
import { CheckboxCircle } from "@styled-icons/remix-fill"
import {
PaneContent,
@@ -43,6 +48,7 @@ import {
PopperHover,
spinAnimation,
Tooltip,
+ Text,
} from "../../components"
import { actions } from "../../store"
import { color, copyToClipboard, ErrorResult, isServerError } from "../../utils"
@@ -50,12 +56,14 @@ import * as QuestDB from "../../utils/questdb"
import Table from "./Table"
import LoadingError from "./LoadingError"
import { Box } from "../../components/Box"
-import { Button } from "@questdb/react-components"
+import { Button, DropdownMenu, ForwardRef } from "@questdb/react-components"
import { Panel } from "../../components/Panel"
import { QuestContext, useSettings } from "../../providers"
import { eventBus } from "../../modules/EventBus"
import { EventType } from "../../modules/EventBus/types"
import { formatTableSchemaQueryResult } from "./Table/ContextualMenu/services"
+import { Toolbar } from "./Toolbar/toolbar"
+import { SchemaContext } from "./SchemaContext"
type Props = Readonly<{
hideMenu?: boolean
@@ -76,7 +84,6 @@ const Content = styled(PaneContent)<{
_loading: boolean
}>`
display: block;
- font-family: ${({ theme }) => theme.fontMonospace};
overflow: auto;
${({ _loading }) => _loading && loadingStyles};
`
@@ -98,54 +105,106 @@ const StyledCheckboxCircle = styled(CheckboxCircle)`
color: ${({ theme }) => theme.color.green};
`
+const DropdownMenuContent = styled(DropdownMenu.Content)`
+ z-index: 100;
+ background: ${({ theme }) => theme.color.backgroundDarker};
+`
+
+const Loading = () => {
+ const [loaderShown, setLoaderShown] = useState(false)
+ // Show the loader only for delayed fetching process
+ useEffect(() => {
+ const timeout = setTimeout(() => setLoaderShown(true), 1000)
+ return () => clearTimeout(timeout)
+ }, [])
+
+ return loaderShown ? : null
+}
+
+enum View {
+ loading,
+ error,
+ ready,
+}
+
+type State = { view: View; loadingError?: ErrorResult }
+
+const initialState: State = {
+ view: View.loading,
+}
+
+const reducer = (s: State, n: Partial) => ({ ...s, ...n })
+
const Schema = ({
innerRef,
...rest
}: Props & { innerRef: Ref }) => {
+ const [state, dispatchState] = useReducer(reducer, initialState)
const { quest } = useContext(QuestContext)
- const [loading, setLoading] = useState(false)
const [loadingError, setLoadingError] = useState(null)
const errorRef = useRef(null)
const [tables, setTables] = useState()
+ const [walTables, setWalTables] = useState()
const [opened, setOpened] = useState()
const [isScrolling, setIsScrolling] = useState(false)
- const [searchVisible, setSearchVisible] = useState(false)
const { consoleConfig } = useSettings()
const dispatch = useDispatch()
const [scrollAtTop, setScrollAtTop] = useState(true)
const scrollerRef = useRef(null)
const [copied, setCopied] = useState(false)
+ const [query, setQuery] = useState("")
+ const [filterSuspendedOnly, setFilterSuspendedOnly] = useState(false)
+ const [columns, setColumns] = useState()
+ const [dropdownOpen, setDropdownOpen] = useState(false)
const handleChange = (name: string) => {
setOpened(name === opened ? undefined : name)
}
- const fetchTables = () => {
- setLoading(true)
- setOpened(undefined)
- combineLatest(
- from(quest.showTables()).pipe(startWith(null)),
- of(true).pipe(delay(1000), startWith(false)),
- ).subscribe(
- ([response]) => {
- if (response && response.type === QuestDB.Type.DQL) {
- setLoadingError(null)
- errorRef.current = null
- setTables(response.data)
- dispatch(actions.query.setTables(response.data))
- } else {
- setLoading(false)
- }
- },
- (error) => {
- if (isServerError(error)) {
- setLoadingError(error)
+ const fetchTables = async () => {
+ try {
+ const response = await quest.showTables()
+ if (response && response.type === QuestDB.Type.DQL) {
+ errorRef.current = null
+ setTables(response.data)
+ dispatch(actions.query.setTables(response.data))
+ // Fetch WAL info about the tables
+ const walTablesResponse = await quest.query(
+ "wal_tables()",
+ )
+ if (walTablesResponse && walTablesResponse.type === QuestDB.Type.DQL) {
+ // Filter out the system tables
+ setWalTables(
+ walTablesResponse.data.filter((wt) =>
+ response.data.map((t) => t.table_name).includes(wt.name),
+ ),
+ )
}
- },
- () => {
- setLoading(false)
- },
- )
+ dispatchState({ view: View.ready })
+ } else {
+ dispatchState({ view: View.error })
+ }
+ } catch (error) {
+ dispatchState({
+ view: View.error,
+ })
+ }
+ }
+
+ const fetchColumns = async () => {
+ try {
+ const response = await quest.query(
+ "information_schema.columns()",
+ )
+ if (response && response && response.type === QuestDB.Type.DQL) {
+ setColumns(response.data)
+ dispatch(actions.query.setColumns(response.data))
+ }
+ } catch (error) {
+ dispatchState({
+ view: View.error,
+ })
+ }
}
const copySchemasToClipboard = async () => {
@@ -174,9 +233,11 @@ const Schema = ({
useEffect(() => {
void fetchTables()
+ void fetchColumns()
eventBus.subscribe(EventType.MSG_QUERY_SCHEMA, () => {
void fetchTables()
+ void fetchColumns()
})
eventBus.subscribe(EventType.MSG_CONNECTION_ERROR, (error) => {
@@ -190,88 +251,155 @@ const Schema = ({
// The connection has been re-established, and we have an error in memory
if (errorRef.current !== null) {
void fetchTables()
+ void fetchColumns()
}
})
- window.addEventListener("focus", fetchTables)
- return () => window.removeEventListener("focus", fetchTables)
+ window.addEventListener("focus", () => {
+ void fetchTables()
+ void fetchColumns()
+ })
+
+ return () =>
+ window.removeEventListener("focus", () => {
+ void fetchTables()
+ void fetchColumns()
+ })
}, [])
+ const views: { [key in View]: () => React.ReactNode } = {
+ [View.loading]: () => ,
+ [View.error]: () =>
+ loadingError ? : ,
+ [View.ready]: () => (
+ <>
+ {tables && tables.length > 0 && (
+ t.suspended).length ?? 0
+ }
+ filterSuspendedOnly={filterSuspendedOnly}
+ setFilterSuspendedOnly={setFilterSuspendedOnly}
+ />
+ )}
+ {tables &&
+ tables
+ .filter((table: QuestDB.Table) => {
+ const normalizedTableName = table.table_name.toLowerCase()
+ const normalizedQuery = query.toLowerCase()
+ const tableColumns = columns
+ ?.filter((c) => c.table_name === table.table_name)
+ .map((c) => c.column_name)
+ return (
+ (normalizedTableName.includes(normalizedQuery) ||
+ tableColumns?.find((c) => c.startsWith(query))) &&
+ (filterSuspendedOnly
+ ? table.walEnabled &&
+ walTables?.find((t) => t.name === table.table_name)
+ ?.suspended
+ : true)
+ )
+ })
+ .map((table: QuestDB.Table) => (
+ wt.name === table.table_name,
+ )}
+ dedup={table.dedup}
+ />
+ ))}
+
+ >
+ ),
+ }
+
return (
-
-
- {consoleConfig.readOnly === false && tables && (
-
- {tables.length > 0 && (
+
+
+
+ {consoleConfig.readOnly === false && tables && (
+
+ dispatch(actions.console.setActiveSidebar("create"))
+ }
skin="transparent"
>
- {copied && }
-
+
}
>
-
- {copied
- ? `Copied ${tables.length} schema${
- tables.length > 1 ? "s" : ""
- } to clipboard`
- : "Copy schemas to clipboard"}
-
+ Create table
- )}
-
-
-
- }
- >
- Refresh
-
-
- )}
-
- }
- shadow={!scrollAtTop}
- />
- setScrollAtTop(scrollerRef?.current?.scrollTop === 0)}
- >
- {loading ? (
-
- ) : loadingError ? (
-
- ) : (
- tables?.map((table) => (
-
- ))
- )}
- {!loading && }
-
-
+
+
+
+
+
+ {copied && }
+
+
+ }
+ >
+ Settings
+
+
+
+
+
+
+ {tables.length > 0 && (
+
+
+
+ Copy schemas to clipboard
+
+
+ )}
+
+
+ Refresh tables
+
+
+
+
+
+ )}
+
+ }
+ shadow={!scrollAtTop}
+ />
+ setScrollAtTop(scrollerRef?.current?.scrollTop === 0)}
+ >
+ {views[state.view]()}
+
+
+
)
}
diff --git a/packages/web-console/src/scenes/Schema/table-icon.tsx b/packages/web-console/src/scenes/Schema/table-icon.tsx
new file mode 100644
index 000000000..59318b0a9
--- /dev/null
+++ b/packages/web-console/src/scenes/Schema/table-icon.tsx
@@ -0,0 +1,148 @@
+import { IconWithTooltip } from "../../components"
+import React from "react"
+import styled from "styled-components"
+import * as QuestDB from "../../utils/questdb"
+import { Table } from "@styled-icons/remix-line"
+import { Box } from "@questdb/react-components"
+
+type Props = {
+ partitionBy?: QuestDB.PartitionBy
+ walEnabled?: boolean
+ suspended?: boolean
+}
+
+const RED = "#ff5555"
+const WHITE = "#f8f8f2"
+
+const WIDTH = "2.2rem"
+const HEIGHT = "1.8rem"
+
+const Root = styled.div<{ $suspended?: boolean }>`
+ display: flex;
+ align-items: center;
+ width: ${WIDTH};
+ height: ${HEIGHT};
+ position: relative;
+ color: ${({ $suspended }) => ($suspended ? RED : WHITE)};
+ margin-right: 1rem;
+ flex-shrink: 0;
+`
+
+const PartitionLetter = styled.span`
+ width: 100%;
+ height: 100%;
+ text-align: center;
+ font-size: 1.4rem;
+ line-height: ${HEIGHT};
+`
+
+const Icon = styled.div<{ $suspended?: boolean }>`
+ position: absolute;
+ width: ${WIDTH};
+ height: ${HEIGHT};
+ border: 1px ${({ $suspended }) => ($suspended ? RED : WHITE)} solid;
+ border-radius: 2px;
+`
+
+const Asterisk = styled.span`
+ position: absolute;
+ top: -9px;
+ right: -0.5rem;
+ font-size: 1.8rem;
+ line-height: 1.8rem;
+ color: #f1fa8c;
+`
+
+const HLine = styled.div`
+ position: absolute;
+ width: 100%;
+ height: 0.1rem;
+ background: WHITE;
+ top: 0.4rem;
+`
+
+const VLine = styled.div`
+ position: absolute;
+ width: 0.1rem;
+ height: calc(${HEIGHT} - 0.5rem);
+ background: WHITE;
+ top: 0.4rem;
+`
+
+const VLine1 = styled(VLine)`
+ left: 0.6rem;
+`
+
+const VLine2 = styled(VLine)`
+ left: 1.3rem;
+`
+
+const IconComponent = ({
+ isPartitioned,
+ partitionBy,
+ walEnabled,
+ suspended,
+}: Props & { isPartitioned: boolean }) => (
+
+ {!walEnabled && *}
+
+ {!isPartitioned && (
+ <>
+
+
+
+ >
+ )}
+
+ {isPartitioned && partitionBy && (
+ {partitionBy.substr(0, 1)}
+ )}
+
+)
+
+export const TableIcon = ({ partitionBy, walEnabled, suspended }: Props) => {
+ const isPartitioned = (partitionBy && partitionBy !== "NONE") || false
+ let tooltipLines = []
+ if (isPartitioned && partitionBy) {
+ tooltipLines.push(
+ `${partitionBy.substr(0, 1)}: Partitioned by ${partitionBy}`,
+ )
+ } else {
+ tooltipLines.push(
+
+
+ : Not partitioned
+ ,
+ )
+ }
+
+ if (!walEnabled) {
+ tooltipLines.push("*: non-WAL (Legacy)")
+ }
+
+ return isPartitioned || !walEnabled ? (
+
+
+
+ }
+ tooltip={tooltipLines.map((line, i) => (
+ {line}
+ ))}
+ placement="bottom"
+ />
+ ) : (
+
+ )
+}
diff --git a/packages/web-console/src/store/Query/actions.ts b/packages/web-console/src/store/Query/actions.ts
index 10436e30a..56f8d04a0 100644
--- a/packages/web-console/src/store/Query/actions.ts
+++ b/packages/web-console/src/store/Query/actions.ts
@@ -24,7 +24,11 @@
import type { ReactNode } from "react"
-import type { QueryRawResult, Table } from "utils/questdb"
+import type {
+ QueryRawResult,
+ Table,
+ InformationSchemaColumn,
+} from "utils/questdb"
import {
NotificationShape,
@@ -40,6 +44,13 @@ const setTables = (payload: Table[]): QueryAction => ({
type: QueryAT.SET_TABLES,
})
+const setColumns = (payload: InformationSchemaColumn[]): QueryAction => ({
+ payload: {
+ columns: payload,
+ },
+ type: QueryAT.SET_COLUMNS,
+})
+
const addNotification = (
payload: Partial & { content: ReactNode },
): QueryAction => ({
@@ -84,4 +95,5 @@ export default {
stopRunning,
toggleRunning,
setTables,
+ setColumns,
}
diff --git a/packages/web-console/src/store/Query/reducers.ts b/packages/web-console/src/store/Query/reducers.ts
index adae1e0ed..14edcf793 100644
--- a/packages/web-console/src/store/Query/reducers.ts
+++ b/packages/web-console/src/store/Query/reducers.ts
@@ -27,6 +27,7 @@ import { QueryAction, QueryAT, QueryStateShape } from "../../types"
export const initialState: QueryStateShape = {
notifications: [],
tables: [],
+ columns: [],
running: {
value: false,
isRefresh: false,
@@ -99,6 +100,13 @@ const query = (state = initialState, action: QueryAction): QueryStateShape => {
}
}
+ case QueryAT.SET_COLUMNS: {
+ return {
+ ...state,
+ columns: action.payload.columns,
+ }
+ }
+
default:
return state
}
diff --git a/packages/web-console/src/store/Query/selectors.ts b/packages/web-console/src/store/Query/selectors.ts
index 3fdf22aa0..7ec1e4563 100644
--- a/packages/web-console/src/store/Query/selectors.ts
+++ b/packages/web-console/src/store/Query/selectors.ts
@@ -23,7 +23,11 @@
******************************************************************************/
import { NotificationShape, RunningShape, StoreShape } from "types"
-import type { QueryRawResult, Table } from "utils/questdb"
+import type {
+ QueryRawResult,
+ Table,
+ InformationSchemaColumn,
+} from "utils/questdb"
const getNotifications: (store: StoreShape) => NotificationShape[] = (store) =>
store.query.notifications
@@ -36,9 +40,13 @@ const getRunning: (store: StoreShape) => RunningShape = (store) =>
const getTables: (store: StoreShape) => Table[] = (store) => store.query.tables
+const getColumns: (store: StoreShape) => InformationSchemaColumn[] = (store) =>
+ store.query.columns
+
export default {
getNotifications,
getResult,
getRunning,
getTables,
+ getColumns,
}
diff --git a/packages/web-console/src/store/Query/types.ts b/packages/web-console/src/store/Query/types.ts
index 23c4ab34c..1553a0836 100644
--- a/packages/web-console/src/store/Query/types.ts
+++ b/packages/web-console/src/store/Query/types.ts
@@ -24,7 +24,11 @@
import type { ReactNode } from "react"
-import type { QueryRawResult, Table } from "utils/questdb"
+import type {
+ QueryRawResult,
+ Table,
+ InformationSchemaColumn,
+} from "utils/questdb"
export enum NotificationType {
ERROR = "error",
@@ -50,6 +54,7 @@ export type RunningShape = Readonly<{
export type QueryStateShape = Readonly<{
notifications: NotificationShape[]
tables: Table[]
+ columns: InformationSchemaColumn[]
result?: QueryRawResult
running: RunningShape
maxNotifications: number
@@ -63,6 +68,7 @@ export enum QueryAT {
STOP_RUNNING = "QUERY/STOP_RUNNING",
TOGGLE_RUNNING = "QUERY/TOGGLE_RUNNING",
SET_TABLES = "QUERY/SET_TABLES",
+ SET_COLUMNS = "QUERY/SET_COLUMNS",
}
type AddNotificationAction = Readonly<{
@@ -102,6 +108,13 @@ type SetTablesAction = Readonly<{
}>
}>
+type SetColumnsActions = Readonly<{
+ type: QueryAT.SET_COLUMNS
+ payload: Readonly<{
+ columns: InformationSchemaColumn[]
+ }>
+}>
+
export type QueryAction =
| AddNotificationAction
| CleanupNotificationsAction
@@ -110,3 +123,4 @@ export type QueryAction =
| StopRunningAction
| ToggleRunningAction
| SetTablesAction
+ | SetColumnsActions
diff --git a/packages/web-console/src/utils/errorWorkarounds.ts b/packages/web-console/src/utils/errorWorkarounds.ts
new file mode 100644
index 000000000..d4e2bf218
--- /dev/null
+++ b/packages/web-console/src/utils/errorWorkarounds.ts
@@ -0,0 +1,41 @@
+import * as QuestDB from "./questdb"
+
+export const errorWorkarounds: Record<
+ QuestDB.ErrorTag,
+ {
+ title: string
+ message: string
+ link: string
+ }
+> = {
+ [QuestDB.ErrorTag.TOO_MANY_OPEN_FILES]: {
+ title: "System limit for open files",
+ message:
+ "Too many open files, please, increase the maximum number of open file handlers OS limit",
+ link: "https://questdb.io/docs/deployment/capacity-planning/#maximum-open-files",
+ },
+ [QuestDB.ErrorTag.DISK_FULL]: {
+ title: "OS configuration",
+ message:
+ "No space left on device, please, extend the volume or free existing disk space up",
+ link: "https://questdb.io/docs/deployment/capacity-planning/#os-configuration",
+ },
+ [QuestDB.ErrorTag.OUT_OF_MMAP_AREAS]: {
+ title: "Max virtual memory areas limit",
+ message:
+ "Out of virtual memory mapping areas, please, increase the maximum number of memory-mapped areas OS limit",
+ link: "https://questdb.io/docs/deployment/capacity-planning/#max-virtual-memory-areas-limit",
+ },
+ [QuestDB.ErrorTag.OUT_OF_MEMORY]: {
+ title: "Out of memory",
+ message:
+ "Out of memory, please, analyze system metrics, and upgrade memory, if necessary",
+ link: "https://questdb.io/docs/deployment/capacity-planning/#cpu-and-ram-configuration",
+ },
+ [QuestDB.ErrorTag.UNSUPPORTED_FILE_SYSTEM]: {
+ title: "Unsupported file system",
+ message:
+ "DB root is located on an unsupported file system, please, move the DB root to a volume with supported file system",
+ link: "https://questdb.io/docs/operations/backup/#supported-filesystems",
+ },
+}
diff --git a/packages/web-console/src/utils/questdb.ts b/packages/web-console/src/utils/questdb.ts
index 2218e6c80..9c4a4e736 100644
--- a/packages/web-console/src/utils/questdb.ts
+++ b/packages/web-console/src/utils/questdb.ts
@@ -118,14 +118,50 @@ export type QueryResult> =
| DmlResult
| DdlResult
+export type PartitionBy = "HOUR" | "DAY" | "WEEK" | "MONTH" | "YEAR" | "NONE"
+
export type Table = {
table_name: string
- partitionBy: string
+ partitionBy: PartitionBy
designatedTimestamp: string
walEnabled: boolean
dedup: boolean
}
+export type Partition = {
+ index: number
+ partitionBy: PartitionBy
+ name: string
+ minTimestamp: string | null
+ maxTimestamp: string | null
+ numRows: string
+ diskSize: string
+ diskSizeHuman: string
+ readOnly: boolean
+ active: boolean
+ attached: boolean
+ detached: boolean
+ attachable: boolean
+}
+
+export enum ErrorTag {
+ DISK_FULL = "DISK FULL",
+ TOO_MANY_OPEN_FILES = "TOO MANY OPEN FILES",
+ OUT_OF_MMAP_AREAS = "OUT OF MMAP AREAS",
+ OUT_OF_MEMORY = "OUT OF MEMORY",
+ UNSUPPORTED_FILE_SYSTEM = "UNSUPPORTED FILE SYSTEM",
+}
+
+export type WalTable = {
+ name: string
+ suspended: boolean
+ writerTxn: string
+ writerLagTxnCount: string
+ sequencerTxn: string
+ errorTag?: ErrorTag
+ errorMessage?: string
+}
+
export type Column = {
column: string
indexed: boolean
@@ -207,6 +243,13 @@ export type SchemaColumn = {
upsertKey?: boolean
}
+export type InformationSchemaColumn = {
+ table_name: string
+ ordinal_position: number
+ column_name: string
+ data_type: string
+}
+
type UploadOptions = {
file: File
name: string
@@ -308,9 +351,9 @@ export class Client {
this._controllers = []
}
- async query(query: string, options?: Options): Promise> {
- const result = await this.queryRaw(query, options)
-
+ static transformQueryRawResult = (
+ result: QueryRawResult,
+ ): QueryResult => {
if (result.type === Type.DQL) {
const { columns, count, dataset, timings } = result
@@ -338,6 +381,16 @@ export class Client {
return result
}
+ async query(query: string, options?: Options): Promise> {
+ const result = await this.queryRaw(query, options)
+
+ return Client.transformQueryRawResult(result)
+ }
+
+ async mockQueryResult(result: QueryRawResult): Promise> {
+ return Client.transformQueryRawResult(result)
+ }
+
async queryRaw(query: string, options?: Options): Promise {
const controller = new AbortController()
const payload = {
@@ -370,10 +423,10 @@ export class Client {
const start = new Date()
try {
- response = await fetch(
- `exec?${Client.encodeParams(payload)}`,
- { signal: controller.signal, headers: this.commonHeaders },
- )
+ response = await fetch(`exec?${Client.encodeParams(payload)}`, {
+ signal: controller.signal,
+ headers: this.commonHeaders,
+ })
} catch (error) {
const err = {
position: -1,
diff --git a/packages/web-console/webpack.config.js b/packages/web-console/webpack.config.js
index 502bb3927..34f3791cb 100644
--- a/packages/web-console/webpack.config.js
+++ b/packages/web-console/webpack.config.js
@@ -81,7 +81,7 @@ module.exports = {
hot: true,
port: config.port,
proxy: {
- context: ["/imp", "/exp", "/exec", "/chk", "/settings"],
+ context: ["/imp", "/exp", "/exec", "/chk", "/settings", "/warnings"],
target: config.backendUrl,
},
client: {
diff --git a/run_browser_tests.sh b/run_browser_tests.sh
index 8c51ce570..3fd77e2b2 100755
--- a/run_browser_tests.sh
+++ b/run_browser_tests.sh
@@ -7,8 +7,8 @@
# ./run_browser_tests.sh -skipQuestDBBuild
rm -rf packages/browser-tests/cypress/snapshots/*
-rm -rf tmp/questdb-*
rm -rf tmp/dbroot
+rm -rf tmp/questdb-*
if [[ $1 = "-skipQuestDBBuild" ]]
then
@@ -24,7 +24,9 @@ yarn install --immutable --immutable-cache
yarn workspace @questdb/react-components run build
yarn workspace @questdb/web-console run build
-./tmp/questdb-*/bin/questdb.sh start -d ./tmp/dbroot
+export QDB_DEV_MODE_ENABLED=true
+
+./tmp/questdb-*/bin/questdb.sh start -d tmp/dbroot
node packages/web-console/serve-dist.js &
PID="$!"
echo "Proxy started, PID=$PID"
diff --git a/submodule_is_mergeable.py b/submodule_is_mergeable.py
index f77636424..e7bdb779a 100755
--- a/submodule_is_mergeable.py
+++ b/submodule_is_mergeable.py
@@ -60,12 +60,12 @@ def check_commit_is_from_recent_master(oss_master_commits: [pygit2.Oid], oss_hea
def check_not_older(
- ent_repo: pygit2.Repository,
+ ui_repo: pygit2.Repository,
oss_master_commits: [pygit2.Oid],
oss_head: pygit2.Oid):
- ent_main_head = ent_repo.references['refs/remotes/origin/main'].peel().id
- ent_main_tree = ent_repo[ent_main_head].tree
- questdb_main_commit_id = ent_main_tree['questdb'].id
+ ui_main_head = ui_repo.references['refs/remotes/origin/main'].peel().id
+ ui_main_tree = ui_repo[ui_main_head].tree
+ questdb_main_commit_id = ui_main_tree['packages/browser-tests/questdb'].id
if questdb_main_commit_id not in oss_master_commits:
sys.stderr.write(
@@ -87,9 +87,9 @@ def check_not_older(
def main():
- # Open up the enterprise and submodule repos.
- ent_repo = pygit2.Repository('.git')
- questdb_repo: pygit2.Repository = ent_repo.lookup_submodule('questdb').open()
+ # Open the ui and questdb submodule repositories.
+ ui_repo = pygit2.Repository('.git')
+ questdb_repo: pygit2.Repository = ui_repo.lookup_submodule('questdb').open()
# The current commit id pointed to by the submodule in this branch.
oss_head: pygit2.Oid = questdb_repo.head.target
@@ -101,8 +101,8 @@ def main():
check_commit_is_from_recent_master(oss_master_commits, oss_head)
# Check that the submodule's commit is not older than the current commit
- # from the `main` branch of the enterprise repo.
- # check_not_older(ent_repo, oss_master_commits, oss_head)
+ # from the `main` branch of the ui repo.
+ # check_not_older(ui_repo, oss_master_commits, oss_head)
if __name__ == '__main__':
diff --git a/yarn.lock b/yarn.lock
index 680339df6..f44122631 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3245,6 +3245,7 @@ __metadata:
"@types/ramda": "npm:0.27.40"
"@types/react": "npm:16.9.35"
"@types/react-dom": "npm:16.9.8"
+ "@types/react-highlight-words": "npm:^0.16.7"
"@types/react-redux": "npm:7.1.9"
"@types/react-transition-group": "npm:4.4.0"
"@types/styled-components": "npm:5.1.7"
@@ -3302,6 +3303,7 @@ __metadata:
react: "npm:17.0.2"
react-contextmenu: "npm:2.14.0"
react-dom: "npm:17.0.2"
+ react-highlight-words: "npm:^0.20.0"
react-hook-form: "npm:7.22.3"
react-is: "npm:^18.1.0"
react-markdown: "npm:^8.0.7"
@@ -6112,6 +6114,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/react-highlight-words@npm:^0.16.7":
+ version: 0.16.7
+ resolution: "@types/react-highlight-words@npm:0.16.7"
+ dependencies:
+ "@types/react": "npm:*"
+ checksum: 10/503e49f396cd9b9d08e5f3ca4817095427d51bfdb094e2458752908e1803d379750aa7b421397fe6e6fe8c842fbe91f5b30f87a56586ae7f2b2f0e6816b69113
+ languageName: node
+ linkType: hard
+
"@types/react-redux@npm:7.1.9":
version: 7.1.9
resolution: "@types/react-redux@npm:7.1.9"
@@ -12482,6 +12493,13 @@ __metadata:
languageName: node
linkType: hard
+"highlight-words-core@npm:^1.2.0":
+ version: 1.2.2
+ resolution: "highlight-words-core@npm:1.2.2"
+ checksum: 10/6a5364ad8cce0107a387d3f43374f5814fb586cc155b282196bc2b9ce1c25aa9a8b76e8ab04d0c29428403a48225cad8280e00b8adbb6b02e05e3a6ab7ee2ef3
+ languageName: node
+ linkType: hard
+
"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
@@ -15361,6 +15379,13 @@ __metadata:
languageName: node
linkType: hard
+"memoize-one@npm:^4.0.0":
+ version: 4.0.3
+ resolution: "memoize-one@npm:4.0.3"
+ checksum: 10/6aee6da4d5305b0876c47166522f417440ad0a11483b5199bfae5896c72f4f5b13a5993b8842178a9f0501e0ab501775b54b6eff09af54cc992fc8f607e72338
+ languageName: node
+ linkType: hard
+
"memoizerific@npm:^1.11.3":
version: 1.11.3
resolution: "memoizerific@npm:1.11.3"
@@ -17484,7 +17509,7 @@ __metadata:
languageName: node
linkType: hard
-"prop-types@npm:^15.0.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
+"prop-types@npm:^15.0.0, prop-types@npm:^15.5.8, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1":
version: 15.8.1
resolution: "prop-types@npm:15.8.1"
dependencies:
@@ -17771,6 +17796,19 @@ __metadata:
languageName: node
linkType: hard
+"react-highlight-words@npm:^0.20.0":
+ version: 0.20.0
+ resolution: "react-highlight-words@npm:0.20.0"
+ dependencies:
+ highlight-words-core: "npm:^1.2.0"
+ memoize-one: "npm:^4.0.0"
+ prop-types: "npm:^15.5.8"
+ peerDependencies:
+ react: ^0.14.0 || ^15.0.0 || ^16.0.0-0 || ^17.0.0-0 || ^18.0.0-0
+ checksum: 10/5adf2cfb1f325ae51ea4dd2cb7522eb433b25534355868d1a3f4556b2b9f7a774c2a1aaa143abebb63a1b3a5590e70ba3d765942a47ff754a1a513cdc5b2f58b
+ languageName: node
+ linkType: hard
+
"react-hook-form@npm:7.22.3":
version: 7.22.3
resolution: "react-hook-form@npm:7.22.3"